From 94fc538f446bc4bd9a7f316390b8ca33f6ff1276 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Fri, 19 Dec 2025 19:42:19 +0100 Subject: [PATCH] refactor core presenters --- .eslintrc.json | 112 ++++++++ .../src/domain/dashboard/DashboardService.ts | 8 +- .../dashboard/dtos/DashboardOverviewDTO.ts | 233 ++++++++++++++++- apps/api/src/domain/driver/DriverProviders.ts | 17 ++ .../src/domain/driver/DriverService.test.ts | 98 +++---- apps/api/src/domain/driver/DriverService.ts | 98 +++++-- .../CompleteOnboardingPresenter.test.ts | 62 ----- .../presenters/CompleteOnboardingPresenter.ts | 23 -- .../DriverRegistrationStatusPresenter.ts | 28 -- .../driver/presenters/DriverStatsPresenter.ts | 8 +- apps/api/src/domain/league/LeagueProviders.ts | 5 + .../src/domain/league/LeagueService.test.ts | 5 +- apps/api/src/domain/league/LeagueService.ts | 129 ++++------ .../league/dtos/AllLeaguesWithCapacityDTO.ts | 45 +++- .../dtos/ApproveLeagueJoinRequestDTO.ts | 4 + .../src/domain/league/dtos/CreateLeagueDTO.ts | 4 + .../src/domain/league/dtos/TotalLeaguesDTO.ts | 8 + .../AllLeaguesWithCapacityPresenter.ts | 15 +- .../ApproveLeagueJoinRequestPresenter.ts | 20 +- .../presenters/CreateLeaguePresenter.ts | 7 +- .../GetLeagueAdminPermissionsPresenter.ts | 17 -- .../GetLeagueMembershipsPresenter.ts | 47 ---- .../GetLeagueOwnerSummaryPresenter.ts | 31 +-- .../presenters/GetLeagueProtestsPresenter.ts | 63 +++-- .../presenters/GetLeagueSeasonsPresenter.ts | 37 +-- .../league/presenters/JoinLeaguePresenter.ts | 26 +- .../presenters/LeagueConfigPresenter.ts | 92 ++----- .../presenters/LeagueJoinRequestsPresenter.ts | 27 -- .../presenters/LeagueSchedulePresenter.ts | 33 +-- .../LeagueScoringConfigPresenter.ts | 33 ++- .../LeagueScoringPresetsPresenter.ts | 19 +- .../presenters/LeagueStandingsPresenter.ts | 31 ++- .../league/presenters/LeagueStatsPresenter.ts | 12 +- .../RejectLeagueJoinRequestPresenter.ts | 23 +- .../presenters/RemoveLeagueMemberPresenter.ts | 22 +- .../presenters/TotalLeaguesPresenter.ts | 14 +- .../TransferLeagueOwnershipPresenter.ts | 24 +- .../UpdateLeagueMemberRolePresenter.ts | 22 +- apps/api/src/domain/race/RaceController.ts | 4 +- apps/api/src/domain/race/RaceProviders.ts | 6 - apps/api/src/domain/race/RaceService.ts | 240 ++++++++++++++++-- .../src/domain/race/dtos/AllRacesPageDTO.ts | 45 +++- .../race/presenters/GetAllRacesPresenter.ts | 34 ++- .../race/presenters/GetTotalRacesPresenter.ts | 13 +- .../ImportRaceResultsApiPresenter.ts | 19 +- .../GetEntitySponsorshipPricingResultDTO.ts | 6 + .../api/src/domain/sponsor/dtos/SponsorDTO.ts | 6 + .../presenters/CreateSponsorPresenter.test.ts | 24 +- .../presenters/CreateSponsorPresenter.ts | 24 +- .../GetEntitySponsorshipPricingPresenter.ts | 37 ++- .../GetPendingSponsorshipRequestsPresenter.ts | 13 + .../GetSponsorDashboardPresenter.ts | 24 +- .../GetSponsorSponsorshipsPresenter.ts | 24 +- .../presenters/GetSponsorsPresenter.ts | 26 +- .../GetSponsorshipPricingPresenter.ts | 28 +- .../team/dtos/GetTeamsLeaderboardOutputDTO.ts | 58 +++++ .../team/presenters/AllTeamsPresenter.ts | 34 +-- .../team/presenters/DriverTeamPresenter.ts | 39 ++- .../team/presenters/TeamDetailsPresenter.ts | 50 ++-- .../presenters/TeamJoinRequestsPresenter.ts | 47 ++-- .../team/presenters/TeamMembersPresenter.ts | 53 ++-- .../presenters/TeamsLeaderboardPresenter.ts | 114 +++++++-- .../components/drivers/DriverProfile.tsx | 8 +- .../components/drivers/ProfileStats.tsx | 6 +- core/racing/application/index.ts | 3 + ...LeaguesWithCapacityAndScoringOutputPort.ts | 18 ++ .../AllLeaguesWithCapacityOutputPort.ts | 6 + .../ports/output/AllRacesPageOutputPort.ts | 22 ++ .../ApproveLeagueJoinRequestOutputPort.ts | 4 + .../CompleteDriverOnboardingOutputPort.ts | 3 + .../ports/output/CreateLeagueOutputPort.ts | 4 + .../output/DashboardOverviewOutputPort.ts} | 44 ++-- .../DriverRegistrationStatusOutputPort.ts | 5 + ...mOutputPort.ts => DriverTeamOutputPort.ts} | 8 +- .../output/DriversLeaderboardOutputPort.ts | 22 ++ .../ports/output/GetAllRacesOutputPort.ts | 13 + .../ports/output/GetAllTeamsOutputPort.ts | 2 + .../ports/output/GetLeagueAdminOutputPort.ts | 7 +- .../output/GetLeagueJoinRequestsOutputPort.ts | 21 +- .../output/GetLeagueMembershipsOutputPort.ts | 20 +- .../output/GetLeagueOwnerSummaryOutputPort.ts | 8 +- .../output/GetLeagueProtestsOutputPort.ts | 63 +++-- .../output/GetLeagueSeasonsOutputPort.ts | 13 + .../ports/output/GetSponsorsOutputPort.ts | 10 + .../output/GetSponsorshipPricingOutputPort.ts | 10 + .../ports/output/GetTeamDetailsOutputPort.ts | 6 +- .../ports/output/GetTotalLeaguesOutputPort.ts | 3 + .../ports/output/GetTotalRacesOutputPort.ts | 3 + .../output/ImportRaceResultsApiOutputPort.ts | 8 + .../ports/output/JoinLeagueOutputPort.ts | 5 + .../LeagueDriverSeasonStatsOutputPort.ts | 7 +- .../output/LeagueFullConfigOutputPort.ts | 11 + .../output/LeagueScoringConfigOutputPort.ts | 16 +- .../output/LeagueScoringPresetsOutputPort.ts | 5 + .../ports/output/LeagueStandingsOutputPort.ts | 10 + .../ports/output/LeagueStatsOutputPort.ts | 5 + .../PendingSponsorshipRequestsOutputPort.ts | 24 ++ .../ports/output/ProfileOverviewOutputPort.ts | 57 +++++ .../ports/output/RaceDetailOutputPort.ts | 15 ++ .../ports/output/RacePenaltiesOutputPort.ts | 7 + .../ports/output/RaceProtestsOutputPort.ts | 7 + .../output/RaceRegistrationsOutputPort.ts | 5 + .../output/RaceResultsDetailOutputPort.ts | 16 ++ .../ports/output/RaceWithSOFOutputPort.ts | 12 + .../ports/output/RacesPageOutputPort.ts | 14 + .../RejectLeagueJoinRequestOutputPort.ts | 4 + .../output/RemoveLeagueMemberOutputPort.ts | 3 + .../output/SponsorDashboardOutputPort.ts | 30 +++ .../output/SponsorSponsorshipsOutputPort.ts | 46 ++++ .../output/TeamJoinRequestsOutputPort.ts | 13 + .../ports/output/TeamMembersOutputPort.ts | 14 + .../output/TeamsLeaderboardOutputPort.ts | 50 ++++ .../ports/output/TotalDriversOutputPort.ts | 3 + .../TransferLeagueOwnershipOutputPort.ts | 3 + .../UpdateLeagueMemberRoleOutputPort.ts | 3 + ...lLeaguesWithCapacityAndScoringPresenter.ts | 47 ---- .../IAllLeaguesWithCapacityPresenter.ts | 34 --- .../presenters/IAllRacesPagePresenter.ts | 29 --- .../presenters/IAllTeamsPresenter.ts | 34 --- .../IApproveLeagueJoinRequestPresenter.ts | 13 - .../ICompleteDriverOnboardingPresenter.ts | 17 -- .../presenters/ICreateLeaguePresenter.ts | 15 -- .../presenters/ICreateSponsorPresenter.ts | 20 -- .../IDriverRegistrationStatusPresenter.ts | 14 - .../presenters/IDriverTeamPresenter.ts | 33 --- .../IDriversLeaderboardPresenter.ts | 45 ---- .../IEntitySponsorshipPricingPresenter.ts | 4 - .../presenters/IGetAllRacesPresenter.ts | 20 -- .../IGetLeagueAdminPermissionsPresenter.ts | 13 - .../presenters/IGetLeagueAdminPresenter.ts | 13 - .../IGetLeagueJoinRequestsPresenter.ts | 21 -- .../IGetLeagueMembershipsPresenter.ts | 21 -- .../IGetLeagueOwnerSummaryPresenter.ts | 17 -- .../presenters/IGetLeagueProtestsPresenter.ts | 15 -- .../presenters/IGetLeagueSchedulePresenter.ts | 19 -- .../presenters/IGetLeagueSeasonsPresenter.ts | 21 -- .../presenters/IGetSponsorsPresenter.ts | 20 -- .../IGetSponsorshipPricingPresenter.ts | 18 -- .../presenters/IGetTotalLeaguesPresenter.ts | 11 - .../presenters/IGetTotalRacesPresenter.ts | 11 - .../IImportRaceResultsApiPresenter.ts | 19 -- .../presenters/IImportRaceResultsPresenter.ts | 9 - .../presenters/IJoinLeaguePresenter.ts | 13 - .../ILeagueDriverSeasonStatsPresenter.ts | 43 ---- .../presenters/ILeagueFullConfigPresenter.ts | 65 ----- .../ILeagueSchedulePreviewPresenter.ts | 5 - .../ILeagueScoringConfigPresenter.ts | 37 --- .../ILeagueScoringPresetsPresenter.ts | 14 - .../presenters/ILeagueStandingsPresenter.ts | 21 -- .../presenters/ILeagueStatsPresenter.ts | 16 -- .../IPendingSponsorshipRequestsPresenter.ts | 7 - .../presenters/IProfileOverviewPresenter.ts | 105 -------- .../presenters/IRaceDetailPresenter.ts | 60 ----- .../presenters/IRacePenaltiesPresenter.ts | 32 --- .../presenters/IRaceProtestsPresenter.ts | 32 --- .../presenters/IRaceRegistrationsPresenter.ts | 13 - .../presenters/IRaceResultsDetailPresenter.ts | 39 --- .../presenters/IRaceWithSOFPresenter.ts | 36 --- .../presenters/IRacesPagePresenter.ts | 35 --- .../IRejectLeagueJoinRequestPresenter.ts | 13 - .../IRemoveLeagueMemberPresenter.ts | 11 - .../presenters/ISponsorDashboardPresenter.ts | 7 - .../ISponsorSponsorshipsPresenter.ts | 7 - .../presenters/ITeamDetailsPresenter.ts | 30 --- .../presenters/ITeamJoinRequestsPresenter.ts | 27 -- .../presenters/ITeamMembersPresenter.ts | 28 -- .../presenters/ITeamsLeaderboardPresenter.ts | 40 --- .../presenters/ITotalDriversPresenter.ts | 13 - .../ITransferLeagueOwnershipPresenter.ts | 12 - .../IUpdateLeagueMemberRolePresenter.ts | 11 - .../CompleteDriverOnboardingUseCase.test.ts | 14 +- .../CompleteDriverOnboardingUseCase.ts | 5 +- .../use-cases/DashboardOverviewUseCase.ts | 60 ++--- ...aguesWithCapacityAndScoringUseCase.test.ts | 22 +- ...AllLeaguesWithCapacityAndScoringUseCase.ts | 6 +- .../GetAllLeaguesWithCapacityUseCase.ts | 14 +- .../use-cases/GetAllRacesPageDataUseCase.ts | 18 +- .../use-cases/GetAllRacesUseCase.ts | 28 +- .../use-cases/GetAllTeamsUseCase.ts | 7 +- .../use-cases/GetDriverTeamUseCase.ts | 22 +- .../GetDriversLeaderboardUseCase.test.ts | 82 ++++-- .../use-cases/GetDriversLeaderboardUseCase.ts | 51 ++-- .../GetLeagueDriverSeasonStatsUseCase.test.ts | 61 ++++- .../GetLeagueDriverSeasonStatsUseCase.ts | 67 ++++- .../GetLeagueFullConfigUseCase.test.ts | 74 +----- .../use-cases/GetLeagueFullConfigUseCase.ts | 24 +- .../use-cases/GetLeagueOwnerSummaryUseCase.ts | 2 +- .../use-cases/GetLeagueProtestsUseCase.ts | 62 ++++- .../use-cases/GetLeagueScheduleUseCase.ts | 6 +- .../GetLeagueScoringConfigUseCase.ts | 10 +- .../use-cases/GetLeagueSeasonsUseCase.ts | 8 +- .../use-cases/GetLeagueStandingsUseCase.ts | 6 +- .../use-cases/GetLeagueStatsUseCase.ts | 6 +- .../GetPendingSponsorshipRequestsUseCase.ts | 8 +- .../use-cases/GetProfileOverviewUseCase.ts | 51 ++-- .../use-cases/GetRaceDetailUseCase.ts | 115 ++------- .../use-cases/GetRacePenaltiesUseCase.ts | 19 +- .../use-cases/GetRaceProtestsUseCase.ts | 19 +- .../GetRaceRegistrationsUseCase.test.ts | 24 +- .../use-cases/GetRaceRegistrationsUseCase.ts | 14 +- .../use-cases/GetRaceResultsDetailUseCase.ts | 35 +-- .../use-cases/GetRaceWithSOFUseCase.ts | 30 +-- .../use-cases/GetRacesPageDataUseCase.ts | 19 +- .../use-cases/GetSponsorDashboardUseCase.ts | 8 +- .../GetSponsorSponsorshipsUseCase.ts | 8 +- .../use-cases/GetSponsorsUseCase.ts | 8 +- .../use-cases/GetSponsorshipPricingUseCase.ts | 10 +- .../use-cases/GetTeamDetailsUseCase.ts | 20 +- .../use-cases/GetTeamJoinRequestsUseCase.ts | 28 +- .../use-cases/GetTeamMembersUseCase.ts | 29 ++- .../use-cases/GetTeamsLeaderboardUseCase.ts | 30 ++- .../use-cases/GetTotalDriversUseCase.ts | 10 +- .../use-cases/GetTotalLeaguesUseCase.ts | 10 +- .../use-cases/GetTotalRacesUseCase.ts | 10 +- .../use-cases/ImportRaceResultsApiUseCase.ts | 9 +- .../IsDriverRegisteredForRaceUseCase.ts | 7 +- .../use-cases/JoinLeagueUseCase.test.ts | 7 +- .../use-cases/JoinLeagueUseCase.ts | 17 +- .../ListLeagueScoringPresetsUseCase.test.ts | 63 +++-- .../ListLeagueScoringPresetsUseCase.ts | 23 +- .../use-cases/PreviewLeagueScheduleUseCase.ts | 9 +- .../RejectLeagueJoinRequestUseCase.ts | 14 +- .../use-cases/RemoveLeagueMemberUseCase.ts | 7 +- .../TransferLeagueOwnershipUseCase.ts | 6 +- .../UpdateLeagueMemberRoleUseCase.ts | 4 +- .../IRaceRegistrationRepository.ts | 5 + core/racing/index.ts | 9 +- tests/RegistrationAndTeamUseCases.test.ts | 54 ++-- 228 files changed, 2817 insertions(+), 3097 deletions(-) delete mode 100644 apps/api/src/domain/driver/presenters/CompleteOnboardingPresenter.test.ts delete mode 100644 apps/api/src/domain/driver/presenters/CompleteOnboardingPresenter.ts delete mode 100644 apps/api/src/domain/driver/presenters/DriverRegistrationStatusPresenter.ts create mode 100644 apps/api/src/domain/league/dtos/ApproveLeagueJoinRequestDTO.ts create mode 100644 apps/api/src/domain/league/dtos/CreateLeagueDTO.ts create mode 100644 apps/api/src/domain/league/dtos/TotalLeaguesDTO.ts delete mode 100644 apps/api/src/domain/league/presenters/GetLeagueAdminPermissionsPresenter.ts delete mode 100644 apps/api/src/domain/league/presenters/GetLeagueMembershipsPresenter.ts delete mode 100644 apps/api/src/domain/league/presenters/LeagueJoinRequestsPresenter.ts create mode 100644 apps/api/src/domain/sponsor/presenters/GetPendingSponsorshipRequestsPresenter.ts create mode 100644 apps/api/src/domain/team/dtos/GetTeamsLeaderboardOutputDTO.ts create mode 100644 core/racing/application/ports/output/AllLeaguesWithCapacityAndScoringOutputPort.ts create mode 100644 core/racing/application/ports/output/AllLeaguesWithCapacityOutputPort.ts create mode 100644 core/racing/application/ports/output/AllRacesPageOutputPort.ts create mode 100644 core/racing/application/ports/output/ApproveLeagueJoinRequestOutputPort.ts create mode 100644 core/racing/application/ports/output/CompleteDriverOnboardingOutputPort.ts create mode 100644 core/racing/application/ports/output/CreateLeagueOutputPort.ts rename core/racing/application/{presenters/IDashboardOverviewPresenter.ts => ports/output/DashboardOverviewOutputPort.ts} (51%) create mode 100644 core/racing/application/ports/output/DriverRegistrationStatusOutputPort.ts rename core/racing/application/ports/output/{GetDriverTeamOutputPort.ts => DriverTeamOutputPort.ts} (62%) create mode 100644 core/racing/application/ports/output/DriversLeaderboardOutputPort.ts create mode 100644 core/racing/application/ports/output/GetAllRacesOutputPort.ts create mode 100644 core/racing/application/ports/output/GetLeagueSeasonsOutputPort.ts create mode 100644 core/racing/application/ports/output/GetSponsorsOutputPort.ts create mode 100644 core/racing/application/ports/output/GetSponsorshipPricingOutputPort.ts create mode 100644 core/racing/application/ports/output/GetTotalLeaguesOutputPort.ts create mode 100644 core/racing/application/ports/output/GetTotalRacesOutputPort.ts create mode 100644 core/racing/application/ports/output/ImportRaceResultsApiOutputPort.ts create mode 100644 core/racing/application/ports/output/JoinLeagueOutputPort.ts create mode 100644 core/racing/application/ports/output/LeagueFullConfigOutputPort.ts create mode 100644 core/racing/application/ports/output/LeagueScoringPresetsOutputPort.ts create mode 100644 core/racing/application/ports/output/LeagueStandingsOutputPort.ts create mode 100644 core/racing/application/ports/output/LeagueStatsOutputPort.ts create mode 100644 core/racing/application/ports/output/PendingSponsorshipRequestsOutputPort.ts create mode 100644 core/racing/application/ports/output/ProfileOverviewOutputPort.ts create mode 100644 core/racing/application/ports/output/RaceDetailOutputPort.ts create mode 100644 core/racing/application/ports/output/RacePenaltiesOutputPort.ts create mode 100644 core/racing/application/ports/output/RaceProtestsOutputPort.ts create mode 100644 core/racing/application/ports/output/RaceRegistrationsOutputPort.ts create mode 100644 core/racing/application/ports/output/RaceResultsDetailOutputPort.ts create mode 100644 core/racing/application/ports/output/RaceWithSOFOutputPort.ts create mode 100644 core/racing/application/ports/output/RacesPageOutputPort.ts create mode 100644 core/racing/application/ports/output/RejectLeagueJoinRequestOutputPort.ts create mode 100644 core/racing/application/ports/output/RemoveLeagueMemberOutputPort.ts create mode 100644 core/racing/application/ports/output/SponsorDashboardOutputPort.ts create mode 100644 core/racing/application/ports/output/SponsorSponsorshipsOutputPort.ts create mode 100644 core/racing/application/ports/output/TeamJoinRequestsOutputPort.ts create mode 100644 core/racing/application/ports/output/TeamMembersOutputPort.ts create mode 100644 core/racing/application/ports/output/TeamsLeaderboardOutputPort.ts create mode 100644 core/racing/application/ports/output/TotalDriversOutputPort.ts create mode 100644 core/racing/application/ports/output/TransferLeagueOwnershipOutputPort.ts create mode 100644 core/racing/application/ports/output/UpdateLeagueMemberRoleOutputPort.ts delete mode 100644 core/racing/application/presenters/IAllLeaguesWithCapacityAndScoringPresenter.ts delete mode 100644 core/racing/application/presenters/IAllLeaguesWithCapacityPresenter.ts delete mode 100644 core/racing/application/presenters/IAllRacesPagePresenter.ts delete mode 100644 core/racing/application/presenters/IAllTeamsPresenter.ts delete mode 100644 core/racing/application/presenters/IApproveLeagueJoinRequestPresenter.ts delete mode 100644 core/racing/application/presenters/ICompleteDriverOnboardingPresenter.ts delete mode 100644 core/racing/application/presenters/ICreateLeaguePresenter.ts delete mode 100644 core/racing/application/presenters/ICreateSponsorPresenter.ts delete mode 100644 core/racing/application/presenters/IDriverRegistrationStatusPresenter.ts delete mode 100644 core/racing/application/presenters/IDriverTeamPresenter.ts delete mode 100644 core/racing/application/presenters/IDriversLeaderboardPresenter.ts delete mode 100644 core/racing/application/presenters/IEntitySponsorshipPricingPresenter.ts delete mode 100644 core/racing/application/presenters/IGetAllRacesPresenter.ts delete mode 100644 core/racing/application/presenters/IGetLeagueAdminPermissionsPresenter.ts delete mode 100644 core/racing/application/presenters/IGetLeagueAdminPresenter.ts delete mode 100644 core/racing/application/presenters/IGetLeagueJoinRequestsPresenter.ts delete mode 100644 core/racing/application/presenters/IGetLeagueMembershipsPresenter.ts delete mode 100644 core/racing/application/presenters/IGetLeagueOwnerSummaryPresenter.ts delete mode 100644 core/racing/application/presenters/IGetLeagueProtestsPresenter.ts delete mode 100644 core/racing/application/presenters/IGetLeagueSchedulePresenter.ts delete mode 100644 core/racing/application/presenters/IGetLeagueSeasonsPresenter.ts delete mode 100644 core/racing/application/presenters/IGetSponsorsPresenter.ts delete mode 100644 core/racing/application/presenters/IGetSponsorshipPricingPresenter.ts delete mode 100644 core/racing/application/presenters/IGetTotalLeaguesPresenter.ts delete mode 100644 core/racing/application/presenters/IGetTotalRacesPresenter.ts delete mode 100644 core/racing/application/presenters/IImportRaceResultsApiPresenter.ts delete mode 100644 core/racing/application/presenters/IImportRaceResultsPresenter.ts delete mode 100644 core/racing/application/presenters/IJoinLeaguePresenter.ts delete mode 100644 core/racing/application/presenters/ILeagueDriverSeasonStatsPresenter.ts delete mode 100644 core/racing/application/presenters/ILeagueFullConfigPresenter.ts delete mode 100644 core/racing/application/presenters/ILeagueSchedulePreviewPresenter.ts delete mode 100644 core/racing/application/presenters/ILeagueScoringConfigPresenter.ts delete mode 100644 core/racing/application/presenters/ILeagueScoringPresetsPresenter.ts delete mode 100644 core/racing/application/presenters/ILeagueStandingsPresenter.ts delete mode 100644 core/racing/application/presenters/ILeagueStatsPresenter.ts delete mode 100644 core/racing/application/presenters/IPendingSponsorshipRequestsPresenter.ts delete mode 100644 core/racing/application/presenters/IProfileOverviewPresenter.ts delete mode 100644 core/racing/application/presenters/IRaceDetailPresenter.ts delete mode 100644 core/racing/application/presenters/IRacePenaltiesPresenter.ts delete mode 100644 core/racing/application/presenters/IRaceProtestsPresenter.ts delete mode 100644 core/racing/application/presenters/IRaceRegistrationsPresenter.ts delete mode 100644 core/racing/application/presenters/IRaceResultsDetailPresenter.ts delete mode 100644 core/racing/application/presenters/IRaceWithSOFPresenter.ts delete mode 100644 core/racing/application/presenters/IRacesPagePresenter.ts delete mode 100644 core/racing/application/presenters/IRejectLeagueJoinRequestPresenter.ts delete mode 100644 core/racing/application/presenters/IRemoveLeagueMemberPresenter.ts delete mode 100644 core/racing/application/presenters/ISponsorDashboardPresenter.ts delete mode 100644 core/racing/application/presenters/ISponsorSponsorshipsPresenter.ts delete mode 100644 core/racing/application/presenters/ITeamDetailsPresenter.ts delete mode 100644 core/racing/application/presenters/ITeamJoinRequestsPresenter.ts delete mode 100644 core/racing/application/presenters/ITeamMembersPresenter.ts delete mode 100644 core/racing/application/presenters/ITeamsLeaderboardPresenter.ts delete mode 100644 core/racing/application/presenters/ITotalDriversPresenter.ts delete mode 100644 core/racing/application/presenters/ITransferLeagueOwnershipPresenter.ts delete mode 100644 core/racing/application/presenters/IUpdateLeagueMemberRolePresenter.ts diff --git a/.eslintrc.json b/.eslintrc.json index 63de98b7c..dd58c23e3 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -43,6 +43,118 @@ ] } }, + { + "files": ["core/*/application/ports/*/*.ts"], + "rules": { + "no-restricted-syntax": [ + "error", + { + "selector": "TSInterfaceDeclaration[id.name=/^Get.*Port$/]", + "message": "Port interface names should not start with 'Get'. Use descriptive names without the 'Get' prefix." + } + ] + } + }, + { + "files": ["core/**/*.ts"], + "rules": { + "no-restricted-syntax": [ + "error", + { + "selector": "TSClassDeclaration[id.name=/Blocker$/], TSInterfaceDeclaration[id.name=/Blocker$/]", + "message": "Blocker classes/interfaces are not allowed in core. Use Guards in backend." + }, + { + "selector": "TSClassDeclaration[id.name=/Presenter$/], TSInterfaceDeclaration[id.name=/Presenter$/]", + "message": "Presenter classes/interfaces are not allowed in core. Presenters belong in API or frontend layers." + }, + { + "selector": "TSClassDeclaration[id.name=/Dto$/], TSInterfaceDeclaration[id.name=/Dto$/]", + "message": "DTO classes/interfaces are not allowed in core. DTOs belong in API or frontend layers." + }, + { + "selector": "TSClassDeclaration[id.name=/ViewModel$/], TSInterfaceDeclaration[id.name=/ViewModel$/]", + "message": "ViewModel classes/interfaces are not allowed in core. View Models belong in frontend." + }, + { + "selector": "TSClassDeclaration[id.name=/CommandModel$/], TSInterfaceDeclaration[id.name=/CommandModel$/]", + "message": "CommandModel classes/interfaces are not allowed in core. Command Models belong in frontend." + } + ] + } + }, + { + "files": ["apps/website/**/*.ts"], + "rules": { + "no-restricted-syntax": [ + "error", + { + "selector": "TSClassDeclaration[id.name=/Guard$/], TSInterfaceDeclaration[id.name=/Guard$/]", + "message": "Guard classes/interfaces are not allowed in frontend. Use Blockers in frontend." + } + ] + } + }, + { + "files": ["apps/api/**/*.ts", "apps/website/lib/dtos/**/*.ts"], + "rules": { + "no-restricted-syntax": [ + "error", + { + "selector": "TSEnumDeclaration[id.name=/^(?!.*Enum$).+/]", + "message": "Transport enums must end with 'Enum'." + } + ] + } + }, + { + "files": ["core/*/application/use-cases/*.ts"], + "rules": { + "no-restricted-syntax": [ + "error", + { + "selector": "TSClassDeclaration[id.name=/^(?!.*UseCase$).+/]", + "message": "Use Case classes must end with 'UseCase'." + } + ] + } + }, + { + "files": ["core/*/application/services/*.ts"], + "rules": { + "no-restricted-syntax": [ + "error", + { + "selector": "TSClassDeclaration[id.name=/^(?!.*Service$).+/]", + "message": "Application Service classes must end with 'Service'." + } + ] + } + }, + { + "files": ["apps/website/lib/view-models/*.ts"], + "rules": { + "no-restricted-syntax": [ + "error", + { + "selector": "TSClassDeclaration[id.name=/^(?!.*ViewModel$).+/]", + "message": "View Model classes must end with 'ViewModel'." + } + ] + } + }, + { + "files": ["apps/website/lib/commands/*.ts"], + "rules": { + "no-restricted-syntax": [ + "error", + { + "selector": "TSClassDeclaration[id.name=/^(?!.*CommandModel$).+/]", + "message": "Command Model classes must end with 'CommandModel'." + } + ] + } + }, { "files": ["**/*.ts", "**/*.tsx"], "parser": "@typescript-eslint/parser", diff --git a/apps/api/src/domain/dashboard/DashboardService.ts b/apps/api/src/domain/dashboard/DashboardService.ts index 9767901bb..52ff30daa 100644 --- a/apps/api/src/domain/dashboard/DashboardService.ts +++ b/apps/api/src/domain/dashboard/DashboardService.ts @@ -1,6 +1,8 @@ import { Injectable, Inject } from '@nestjs/common'; +import { plainToClass } from 'class-transformer'; import { DashboardOverviewUseCase } from '@core/racing/application/use-cases/DashboardOverviewUseCase'; -import type { DashboardOverviewViewModel } from '@core/racing/application/presenters/IDashboardOverviewPresenter'; +import type { DashboardOverviewOutputPort } from '@core/racing/application/ports/output/DashboardOverviewOutputPort'; +import { DashboardOverviewDTO } from './dtos/DashboardOverviewDTO'; // Core imports import type { Logger } from '@core/shared/application/Logger'; @@ -62,7 +64,7 @@ export class DashboardService { ); } - async getDashboardOverview(driverId: string): Promise { + async getDashboardOverview(driverId: string): Promise { this.logger.debug('[DashboardService] Getting dashboard overview:', { driverId }); const result = await this.dashboardOverviewUseCase.execute({ driverId }); @@ -71,6 +73,6 @@ export class DashboardService { throw new Error(result.error?.message || 'Failed to get dashboard overview'); } - return result.value!; + return plainToClass(DashboardOverviewDTO, result.value); } } \ No newline at end of file diff --git a/apps/api/src/domain/dashboard/dtos/DashboardOverviewDTO.ts b/apps/api/src/domain/dashboard/dtos/DashboardOverviewDTO.ts index 162d8ccb6..f20995bb9 100644 --- a/apps/api/src/domain/dashboard/dtos/DashboardOverviewDTO.ts +++ b/apps/api/src/domain/dashboard/dtos/DashboardOverviewDTO.ts @@ -1,23 +1,224 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNumber } from 'class-validator'; -import { DashboardDriverSummaryDTO } from './DashboardDriverSummaryDTO'; -import { DashboardRaceSummaryDTO } from './DashboardRaceSummaryDTO'; -import { DashboardRecentResultDTO } from './DashboardRecentResultDTO'; -import { DashboardLeagueStandingSummaryDTO } from './DashboardLeagueStandingSummaryDTO'; -import { DashboardFeedSummaryDTO } from './DashboardFeedSummaryDTO'; -import { DashboardFriendSummaryDTO } from './DashboardFriendSummaryDTO'; +import { IsString, IsNumber, IsOptional, IsBoolean, IsArray, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class DashboardDriverSummaryDTO { + @ApiProperty() + @IsString() + id!: string; + + @ApiProperty() + @IsString() + name!: string; + + @ApiProperty() + @IsString() + country!: string; + + @ApiProperty() + @IsString() + avatarUrl!: string; + + @ApiProperty({ nullable: true }) + @IsOptional() + @IsNumber() + rating?: number | null; + + @ApiProperty({ nullable: true }) + @IsOptional() + @IsNumber() + globalRank?: number | null; + + @ApiProperty() + @IsNumber() + totalRaces!: number; + + @ApiProperty() + @IsNumber() + wins!: number; + + @ApiProperty() + @IsNumber() + podiums!: number; + + @ApiProperty({ nullable: true }) + @IsOptional() + @IsNumber() + consistency?: number | null; +} + +export class DashboardRaceSummaryDTO { + @ApiProperty() + @IsString() + id!: string; + + @ApiProperty() + @IsString() + leagueId!: string; + + @ApiProperty() + @IsString() + leagueName!: string; + + @ApiProperty() + @IsString() + track!: string; + + @ApiProperty() + @IsString() + car!: string; + + @ApiProperty() + @IsString() + scheduledAt!: string; + + @ApiProperty() + @IsString() + status!: 'scheduled' | 'running' | 'completed' | 'cancelled'; + + @ApiProperty() + @IsBoolean() + isMyLeague!: boolean; +} + +export class DashboardRecentResultDTO { + @ApiProperty() + @IsString() + raceId!: string; + + @ApiProperty() + @IsString() + raceName!: string; + + @ApiProperty() + @IsString() + leagueId!: string; + + @ApiProperty() + @IsString() + leagueName!: string; + + @ApiProperty() + @IsString() + finishedAt!: string; + + @ApiProperty() + @IsNumber() + position!: number; + + @ApiProperty() + @IsNumber() + incidents!: number; +} + +export class DashboardLeagueStandingSummaryDTO { + @ApiProperty() + @IsString() + leagueId!: string; + + @ApiProperty() + @IsString() + leagueName!: string; + + @ApiProperty() + @IsNumber() + position!: number; + + @ApiProperty() + @IsNumber() + totalDrivers!: number; + + @ApiProperty() + @IsNumber() + points!: number; +} + +export class DashboardFeedItemSummaryDTO { + @ApiProperty() + @IsString() + id!: string; + + @ApiProperty() + @IsString() + type!: string; + + @ApiProperty() + @IsString() + headline!: string; + + @ApiProperty({ nullable: true }) + @IsOptional() + @IsString() + body?: string; + + @ApiProperty() + @IsString() + timestamp!: string; + + @ApiProperty({ nullable: true }) + @IsOptional() + @IsString() + ctaLabel?: string; + + @ApiProperty({ nullable: true }) + @IsOptional() + @IsString() + ctaHref?: string; +} + +export class DashboardFeedSummaryDTO { + @ApiProperty() + @IsNumber() + notificationCount!: number; + + @ApiProperty({ type: [DashboardFeedItemSummaryDTO] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => DashboardFeedItemSummaryDTO) + items!: DashboardFeedItemSummaryDTO[]; +} + +export class DashboardFriendSummaryDTO { + @ApiProperty() + @IsString() + id!: string; + + @ApiProperty() + @IsString() + name!: string; + + @ApiProperty() + @IsString() + country!: string; + + @ApiProperty() + @IsString() + avatarUrl!: string; +} export class DashboardOverviewDTO { @ApiProperty({ nullable: true }) - currentDriver!: DashboardDriverSummaryDTO | null; + @IsOptional() + @ValidateNested() + @Type(() => DashboardDriverSummaryDTO) + currentDriver?: DashboardDriverSummaryDTO | null; @ApiProperty({ type: [DashboardRaceSummaryDTO] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => DashboardRaceSummaryDTO) myUpcomingRaces!: DashboardRaceSummaryDTO[]; @ApiProperty({ type: [DashboardRaceSummaryDTO] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => DashboardRaceSummaryDTO) otherUpcomingRaces!: DashboardRaceSummaryDTO[]; @ApiProperty({ type: [DashboardRaceSummaryDTO] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => DashboardRaceSummaryDTO) upcomingRaces!: DashboardRaceSummaryDTO[]; @ApiProperty() @@ -25,17 +226,31 @@ export class DashboardOverviewDTO { activeLeaguesCount!: number; @ApiProperty({ nullable: true }) - nextRace!: DashboardRaceSummaryDTO | null; + @IsOptional() + @ValidateNested() + @Type(() => DashboardRaceSummaryDTO) + nextRace?: DashboardRaceSummaryDTO | null; @ApiProperty({ type: [DashboardRecentResultDTO] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => DashboardRecentResultDTO) recentResults!: DashboardRecentResultDTO[]; @ApiProperty({ type: [DashboardLeagueStandingSummaryDTO] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => DashboardLeagueStandingSummaryDTO) leagueStandingsSummaries!: DashboardLeagueStandingSummaryDTO[]; @ApiProperty() + @ValidateNested() + @Type(() => DashboardFeedSummaryDTO) feedSummary!: DashboardFeedSummaryDTO; @ApiProperty({ type: [DashboardFriendSummaryDTO] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => DashboardFriendSummaryDTO) friends!: DashboardFriendSummaryDTO[]; } \ No newline at end of file diff --git a/apps/api/src/domain/driver/DriverProviders.ts b/apps/api/src/domain/driver/DriverProviders.ts index e25677980..51b88da29 100644 --- a/apps/api/src/domain/driver/DriverProviders.ts +++ b/apps/api/src/domain/driver/DriverProviders.ts @@ -17,6 +17,7 @@ import { GetTotalDriversUseCase } from '@core/racing/application/use-cases/GetTo import { CompleteDriverOnboardingUseCase } from '@core/racing/application/use-cases/CompleteDriverOnboardingUseCase'; import { UpdateDriverProfileUseCase } from '@core/racing/application/use-cases/UpdateDriverProfileUseCase'; import { IsDriverRegisteredForRaceUseCase } from '@core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase'; +import { GetProfileOverviewUseCase } from '@core/racing/application/use-cases/GetProfileOverviewUseCase'; // Import concrete in-memory implementations import { InMemoryDriverRepository } from '@adapters/racing/persistence/inmemory/InMemoryDriverRepository'; @@ -44,6 +45,7 @@ export const GET_TOTAL_DRIVERS_USE_CASE_TOKEN = 'GetTotalDriversUseCase'; export const COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN = 'CompleteDriverOnboardingUseCase'; export const IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN = 'IsDriverRegisteredForRaceUseCase'; export const UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN = 'UpdateDriverProfileUseCase'; +export const GET_PROFILE_OVERVIEW_USE_CASE_TOKEN = 'GetProfileOverviewUseCase'; export const DriverProviders: Provider[] = [ DriverService, // Provide the service itself @@ -113,4 +115,19 @@ export const DriverProviders: Provider[] = [ useFactory: (driverRepo: IDriverRepository) => new UpdateDriverProfileUseCase(driverRepo), inject: [DRIVER_REPOSITORY_TOKEN], }, + { + provide: GET_PROFILE_OVERVIEW_USE_CASE_TOKEN, + useFactory: (driverRepo: IDriverRepository, imageService: IImageServicePort, logger: Logger) => + new GetProfileOverviewUseCase( + driverRepo, + // TODO: Add teamRepository, teamMembershipRepository, socialRepository, etc. + null as any, // teamRepository + null as any, // teamMembershipRepository + null as any, // socialRepository + imageService, + () => null, // getDriverStats + () => [], // getAllDriverRankings + ), + inject: [DRIVER_REPOSITORY_TOKEN, IMAGE_SERVICE_PORT_TOKEN, LOGGER_TOKEN], + }, ]; diff --git a/apps/api/src/domain/driver/DriverService.test.ts b/apps/api/src/domain/driver/DriverService.test.ts index 82965bf0e..f1e101b95 100644 --- a/apps/api/src/domain/driver/DriverService.test.ts +++ b/apps/api/src/domain/driver/DriverService.test.ts @@ -8,6 +8,9 @@ import { IsDriverRegisteredForRaceUseCase } from '@core/racing/application/use-c import { UpdateDriverProfileUseCase } from '@core/racing/application/use-cases/UpdateDriverProfileUseCase'; import type { Logger } from '@core/shared/application'; import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository'; +import { Result } from '@core/shared/application/Result'; +import type { CompleteDriverOnboardingOutputPort } from '@core/racing/application/ports/output/CompleteDriverOnboardingOutputPort'; +import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; describe('DriverService', () => { let service: DriverService; @@ -15,7 +18,9 @@ describe('DriverService', () => { let getTotalDriversUseCase: ReturnType>; let completeDriverOnboardingUseCase: ReturnType>; let isDriverRegisteredForRaceUseCase: ReturnType>; + // eslint-disable-next-line @typescript-eslint/no-unused-vars let updateDriverProfileUseCase: ReturnType>; + // eslint-disable-next-line @typescript-eslint/no-unused-vars let driverRepository: ReturnType>; let logger: ReturnType>; @@ -102,17 +107,11 @@ describe('DriverService', () => { activeCount: 1, }; - const mockPresenter = { - viewModel: mockViewModel, - }; - - getDriversLeaderboardUseCase.execute.mockImplementation(async (input, presenter) => { - Object.assign(presenter, mockPresenter); - }); + getDriversLeaderboardUseCase.execute.mockResolvedValue(Result.ok(mockViewModel)); const result = await service.getDriversLeaderboard(); - expect(getDriversLeaderboardUseCase.execute).toHaveBeenCalledWith(undefined, expect.any(Object)); + expect(getDriversLeaderboardUseCase.execute).toHaveBeenCalledWith(); expect(logger.debug).toHaveBeenCalledWith('[DriverService] Fetching drivers leaderboard.'); expect(result).toEqual(mockViewModel); }); @@ -120,26 +119,20 @@ describe('DriverService', () => { describe('getTotalDrivers', () => { it('should call GetTotalDriversUseCase and return the view model', async () => { - const mockViewModel = { totalDrivers: 5 }; + const mockOutput = { totalDrivers: 5 }; - const mockPresenter = { - viewModel: mockViewModel, - }; - - getTotalDriversUseCase.execute.mockImplementation(async (input, presenter) => { - Object.assign(presenter, mockPresenter); - }); + getTotalDriversUseCase.execute.mockResolvedValue(Result.ok(mockOutput)); const result = await service.getTotalDrivers(); - expect(getTotalDriversUseCase.execute).toHaveBeenCalledWith(undefined, expect.any(Object)); + expect(getTotalDriversUseCase.execute).toHaveBeenCalledWith(); expect(logger.debug).toHaveBeenCalledWith('[DriverService] Fetching total drivers count.'); - expect(result).toEqual(mockViewModel); + expect(result).toEqual(mockOutput); }); }); describe('completeOnboarding', () => { - it('should call CompleteDriverOnboardingUseCase and return the view model', async () => { + it('should call CompleteDriverOnboardingUseCase and return success', async () => { const input = { firstName: 'John', lastName: 'Doe', @@ -149,30 +142,43 @@ describe('DriverService', () => { bio: 'Racing enthusiast', }; - const mockViewModel = { - success: true, - driverId: 'user-123', - }; - - const mockPresenter = { - viewModel: mockViewModel, - }; - - completeDriverOnboardingUseCase.execute.mockImplementation(async (input, presenter) => { - Object.assign(presenter, mockPresenter); - }); + completeDriverOnboardingUseCase.execute.mockResolvedValue( + Result.ok>({ driverId: 'user-123' }) + ); const result = await service.completeOnboarding('user-123', input); - expect(completeDriverOnboardingUseCase.execute).toHaveBeenCalledWith( - { - userId: 'user-123', - ...input, - }, - expect.any(Object) - ); + expect(completeDriverOnboardingUseCase.execute).toHaveBeenCalledWith({ + userId: 'user-123', + ...input, + }); expect(logger.debug).toHaveBeenCalledWith('Completing onboarding for user:', 'user-123'); - expect(result).toEqual(mockViewModel); + expect(result).toEqual({ + success: true, + driverId: 'user-123', + }); + }); + + it('should handle error from use case', async () => { + const input = { + firstName: 'John', + lastName: 'Doe', + displayName: 'John Doe', + country: 'US', + timezone: 'America/New_York', + bio: 'Racing enthusiast', + }; + + completeDriverOnboardingUseCase.execute.mockResolvedValue( + Result.err>({ code: 'DRIVER_ALREADY_EXISTS' }) + ); + + const result = await service.completeOnboarding('user-123', input); + + expect(result).toEqual({ + success: false, + errorMessage: 'DRIVER_ALREADY_EXISTS', + }); }); }); @@ -183,25 +189,19 @@ describe('DriverService', () => { raceId: 'race-1', }; - const mockViewModel = { + const mockOutput = { isRegistered: true, raceId: 'race-1', driverId: 'driver-1', }; - const mockPresenter = { - viewModel: mockViewModel, - }; - - isDriverRegisteredForRaceUseCase.execute.mockImplementation(async (params, presenter) => { - Object.assign(presenter, mockPresenter); - }); + isDriverRegisteredForRaceUseCase.execute.mockResolvedValue(Result.ok(mockOutput)); const result = await service.getDriverRegistrationStatus(query); - expect(isDriverRegisteredForRaceUseCase.execute).toHaveBeenCalledWith(query, expect.any(Object)); + expect(isDriverRegisteredForRaceUseCase.execute).toHaveBeenCalledWith(query); expect(logger.debug).toHaveBeenCalledWith('Checking driver registration status:', query); - expect(result).toEqual(mockViewModel); + expect(result).toEqual(mockOutput); }); }); }); \ No newline at end of file diff --git a/apps/api/src/domain/driver/DriverService.ts b/apps/api/src/domain/driver/DriverService.ts index bd6bac889..87d334c32 100644 --- a/apps/api/src/domain/driver/DriverService.ts +++ b/apps/api/src/domain/driver/DriverService.ts @@ -13,15 +13,14 @@ import { GetDriversLeaderboardUseCase } from '@core/racing/application/use-cases import { GetTotalDriversUseCase } from '@core/racing/application/use-cases/GetTotalDriversUseCase'; import { CompleteDriverOnboardingUseCase } from '@core/racing/application/use-cases/CompleteDriverOnboardingUseCase'; import { IsDriverRegisteredForRaceUseCase } from '@core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase'; +import { GetProfileOverviewUseCase } from '@core/racing/application/use-cases/GetProfileOverviewUseCase'; // Presenters -import { DriversLeaderboardPresenter } from './presenters/DriversLeaderboardPresenter'; import { DriverStatsPresenter } from './presenters/DriverStatsPresenter'; -import { CompleteOnboardingPresenter } from './presenters/CompleteOnboardingPresenter'; -import { DriverRegistrationStatusPresenter } from './presenters/DriverRegistrationStatusPresenter'; // Tokens -import { GET_DRIVERS_LEADERBOARD_USE_CASE_TOKEN, GET_TOTAL_DRIVERS_USE_CASE_TOKEN, COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN, IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN, UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN, LOGGER_TOKEN, DRIVER_REPOSITORY_TOKEN } from './DriverProviders'; +import { GET_DRIVERS_LEADERBOARD_USE_CASE_TOKEN, GET_TOTAL_DRIVERS_USE_CASE_TOKEN, COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN, IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN, UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN, GET_PROFILE_OVERVIEW_USE_CASE_TOKEN, LOGGER_TOKEN, DRIVER_REPOSITORY_TOKEN } from './DriverProviders'; +import type { ProfileOverviewOutputPort } from '@core/racing/application/ports/output/ProfileOverviewOutputPort'; import type { Logger } from '@core/shared/application'; import { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository'; import { UpdateDriverProfileUseCase } from '@core/racing/application/use-cases/UpdateDriverProfileUseCase'; @@ -34,6 +33,7 @@ export class DriverService { @Inject(COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN) private readonly completeDriverOnboardingUseCase: CompleteDriverOnboardingUseCase, @Inject(IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN) private readonly isDriverRegisteredForRaceUseCase: IsDriverRegisteredForRaceUseCase, @Inject(UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN) private readonly updateDriverProfileUseCase: UpdateDriverProfileUseCase, + @Inject(GET_PROFILE_OVERVIEW_USE_CASE_TOKEN) private readonly getProfileOverviewUseCase: GetProfileOverviewUseCase, @Inject(DRIVER_REPOSITORY_TOKEN) private readonly driverRepository: IDriverRepository, @Inject(LOGGER_TOKEN) private readonly logger: Logger, ) {} @@ -41,24 +41,31 @@ export class DriverService { async getDriversLeaderboard(): Promise { this.logger.debug('[DriverService] Fetching drivers leaderboard.'); - const presenter = new DriversLeaderboardPresenter(); - await this.getDriversLeaderboardUseCase.execute(undefined, presenter); - return presenter.viewModel; + const result = await this.getDriversLeaderboardUseCase.execute(); + if (result.isOk()) { + return result.value as DriversLeaderboardDTO; + } else { + throw new Error(`Failed to fetch drivers leaderboard: ${result.error.details.message}`); + } } async getTotalDrivers(): Promise { this.logger.debug('[DriverService] Fetching total drivers count.'); + const result = await this.getTotalDriversUseCase.execute(); + if (result.isErr()) { + throw new Error(result.unwrapErr().code); + } + const presenter = new DriverStatsPresenter(); - await this.getTotalDriversUseCase.execute(undefined, presenter); + presenter.present(result.unwrap()); return presenter.viewModel; } async completeOnboarding(userId: string, input: CompleteOnboardingInputDTO): Promise { this.logger.debug('Completing onboarding for user:', userId); - const presenter = new CompleteOnboardingPresenter(); - await this.completeDriverOnboardingUseCase.execute({ + const result = await this.completeDriverOnboardingUseCase.execute({ userId, firstName: input.firstName, lastName: input.lastName, @@ -66,16 +73,31 @@ export class DriverService { country: input.country, timezone: input.timezone, bio: input.bio, - }, presenter); - return presenter.viewModel; + }); + + if (result.isOk()) { + return { + success: true, + driverId: result.value.driverId, + }; + } else { + return { + success: false, + errorMessage: result.error.code, + }; + } } async getDriverRegistrationStatus(query: GetDriverRegistrationStatusQueryDTO): Promise { this.logger.debug('Checking driver registration status:', query); - const presenter = new DriverRegistrationStatusPresenter(); - await this.isDriverRegisteredForRaceUseCase.execute({ raceId: query.raceId, driverId: query.driverId }, presenter); - return presenter.viewModel; + const result = await this.isDriverRegisteredForRaceUseCase.execute({ raceId: query.raceId, driverId: query.driverId }); + if (result.isOk()) { + return result.value; + } else { + // For now, throw error or handle appropriately. Since it's a query, perhaps return default or throw. + throw new Error(`Failed to check registration status: ${result.error.code}`); + } } async getCurrentDriver(userId: string): Promise { @@ -129,18 +151,42 @@ export class DriverService { async getDriverProfile(driverId: string): Promise { this.logger.debug(`[DriverService] Fetching driver profile for driverId: ${driverId}`); - // TODO: Implement proper driver profile fetching with all the detailed data - // For now, return a placeholder structure + const result = await this.getProfileOverviewUseCase.execute({ driverId }); + if (result.isErr()) { + throw new Error(`Failed to fetch driver profile: ${result.error.code}`); + } + + const outputPort = result.value; + return this.mapProfileOverviewToDTO(outputPort); + } + + private mapProfileOverviewToDTO(outputPort: ProfileOverviewOutputPort): GetDriverProfileOutputDTO { return { - currentDriver: null, - stats: null, - finishDistribution: null, - teamMemberships: [], - socialSummary: { - friendsCount: 0, - friends: [], - }, - extendedProfile: null, + currentDriver: outputPort.driver ? { + id: outputPort.driver.id, + name: outputPort.driver.name, + country: outputPort.driver.country, + avatarUrl: outputPort.driver.avatarUrl, + iracingId: outputPort.driver.iracingId, + joinedAt: outputPort.driver.joinedAt.toISOString(), + rating: outputPort.driver.rating, + globalRank: outputPort.driver.globalRank, + consistency: outputPort.driver.consistency, + bio: outputPort.driver.bio, + totalDrivers: outputPort.driver.totalDrivers, + } : null, + stats: outputPort.stats, + finishDistribution: outputPort.finishDistribution, + teamMemberships: outputPort.teamMemberships.map(membership => ({ + teamId: membership.teamId, + teamName: membership.teamName, + teamTag: membership.teamTag, + role: membership.role, + joinedAt: membership.joinedAt.toISOString(), + isCurrent: membership.isCurrent, + })), + socialSummary: outputPort.socialSummary, + extendedProfile: outputPort.extendedProfile, }; } } diff --git a/apps/api/src/domain/driver/presenters/CompleteOnboardingPresenter.test.ts b/apps/api/src/domain/driver/presenters/CompleteOnboardingPresenter.test.ts deleted file mode 100644 index 8a78f8b66..000000000 --- a/apps/api/src/domain/driver/presenters/CompleteOnboardingPresenter.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import { CompleteOnboardingPresenter } from './CompleteOnboardingPresenter'; -import type { CompleteDriverOnboardingResultDTO } from '../../../../../core/racing/application/presenters/ICompleteDriverOnboardingPresenter'; - -describe('CompleteOnboardingPresenter', () => { - let presenter: CompleteOnboardingPresenter; - - beforeEach(() => { - presenter = new CompleteOnboardingPresenter(); - }); - - describe('present', () => { - it('should map successful core DTO to API view model', () => { - const dto: CompleteDriverOnboardingResultDTO = { - success: true, - driverId: 'driver-123', - }; - - presenter.present(dto); - - const result = presenter.viewModel; - - expect(result).toEqual({ - success: true, - driverId: 'driver-123', - errorMessage: undefined, - }); - }); - - it('should map failed core DTO to API view model', () => { - const dto: CompleteDriverOnboardingResultDTO = { - success: false, - errorMessage: 'Driver already exists', - }; - - presenter.present(dto); - - const result = presenter.viewModel; - - expect(result).toEqual({ - success: false, - driverId: undefined, - errorMessage: 'Driver already exists', - }); - }); - }); - - describe('reset', () => { - it('should reset the result', () => { - const dto: CompleteDriverOnboardingResultDTO = { - success: true, - driverId: 'driver-123', - }; - - presenter.present(dto); - expect(presenter.viewModel).toBeDefined(); - - presenter.reset(); - expect(() => presenter.viewModel).toThrow('Presenter not presented'); - }); - }); -}); \ No newline at end of file diff --git a/apps/api/src/domain/driver/presenters/CompleteOnboardingPresenter.ts b/apps/api/src/domain/driver/presenters/CompleteOnboardingPresenter.ts deleted file mode 100644 index 335bcaba6..000000000 --- a/apps/api/src/domain/driver/presenters/CompleteOnboardingPresenter.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { CompleteOnboardingOutputDTO } from '../dtos/CompleteOnboardingOutputDTO'; -import type { ICompleteDriverOnboardingPresenter, CompleteDriverOnboardingResultDTO } from '../../../../../core/racing/application/presenters/ICompleteDriverOnboardingPresenter'; - -export class CompleteOnboardingPresenter implements ICompleteDriverOnboardingPresenter { - private result: CompleteOnboardingOutputDTO | null = null; - - reset() { - this.result = null; - } - - present(dto: CompleteDriverOnboardingResultDTO) { - this.result = { - success: dto.success, - driverId: dto.driverId, - errorMessage: dto.errorMessage, - }; - } - - get viewModel(): CompleteOnboardingOutputDTO { - if (!this.result) throw new Error('Presenter not presented'); - return this.result; - } -} \ No newline at end of file diff --git a/apps/api/src/domain/driver/presenters/DriverRegistrationStatusPresenter.ts b/apps/api/src/domain/driver/presenters/DriverRegistrationStatusPresenter.ts deleted file mode 100644 index af8ee956e..000000000 --- a/apps/api/src/domain/driver/presenters/DriverRegistrationStatusPresenter.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { DriverRegistrationStatusDTO } from '../dtos/DriverRegistrationStatusDTO'; -import type { IDriverRegistrationStatusPresenter } from '../../../../../core/racing/application/presenters/IDriverRegistrationStatusPresenter'; - -export class DriverRegistrationStatusPresenter implements IDriverRegistrationStatusPresenter { - private result: DriverRegistrationStatusDTO | null = null; - - present(isRegistered: boolean, raceId: string, driverId: string) { - this.result = { - isRegistered, - raceId, - driverId, - }; - } - - getViewModel(): DriverRegistrationStatusDTO { - if (!this.result) throw new Error('Presenter not presented'); - return this.result; - } - - // For consistency with other presenters - reset() { - this.result = null; - } - - get viewModel(): DriverRegistrationStatusDTO { - return this.getViewModel(); - } -} \ No newline at end of file diff --git a/apps/api/src/domain/driver/presenters/DriverStatsPresenter.ts b/apps/api/src/domain/driver/presenters/DriverStatsPresenter.ts index d1b27dabe..ead17bf41 100644 --- a/apps/api/src/domain/driver/presenters/DriverStatsPresenter.ts +++ b/apps/api/src/domain/driver/presenters/DriverStatsPresenter.ts @@ -1,16 +1,16 @@ import { DriverStatsDTO } from '../dtos/DriverStatsDTO'; -import type { ITotalDriversPresenter, TotalDriversResultDTO } from '../../../../../core/racing/application/presenters/ITotalDriversPresenter'; +import type { TotalDriversOutputPort } from '../../../../../core/racing/application/ports/output/TotalDriversOutputPort'; -export class DriverStatsPresenter implements ITotalDriversPresenter { +export class DriverStatsPresenter { private result: DriverStatsDTO | null = null; reset() { this.result = null; } - present(dto: TotalDriversResultDTO) { + present(output: TotalDriversOutputPort) { this.result = { - totalDrivers: dto.totalDrivers, + totalDrivers: output.totalDrivers, }; } diff --git a/apps/api/src/domain/league/LeagueProviders.ts b/apps/api/src/domain/league/LeagueProviders.ts index 1e373f198..ac9bfe29a 100644 --- a/apps/api/src/domain/league/LeagueProviders.ts +++ b/apps/api/src/domain/league/LeagueProviders.ts @@ -16,6 +16,7 @@ import { InMemoryRaceRepository } from '@adapters/racing/persistence/inmemory/In import { InMemoryDriverRepository } from '@adapters/racing/persistence/inmemory/InMemoryDriverRepository'; import { InMemoryStandingRepository } from '@adapters/racing/persistence/inmemory/InMemoryStandingRepository'; import { ConsoleLogger } from '@adapters/logging/ConsoleLogger'; +import { listLeagueScoringPresets } from '@adapters/bootstrap/LeagueScoringPresets'; // Import use cases import { GetAllLeaguesWithCapacityUseCase } from '@core/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase'; @@ -131,4 +132,8 @@ export const LeagueProviders: Provider[] = [ GetLeagueScheduleUseCase, GetLeagueStatsUseCase, GetLeagueAdminPermissionsUseCase, + { + provide: ListLeagueScoringPresetsUseCase, + useFactory: () => new ListLeagueScoringPresetsUseCase(listLeagueScoringPresets()), + }, ]; diff --git a/apps/api/src/domain/league/LeagueService.test.ts b/apps/api/src/domain/league/LeagueService.test.ts index 66da94972..2764c5e9a 100644 --- a/apps/api/src/domain/league/LeagueService.test.ts +++ b/apps/api/src/domain/league/LeagueService.test.ts @@ -14,6 +14,7 @@ import { UpdateLeagueMemberRoleUseCase } from '@core/racing/application/use-case import { GetLeagueOwnerSummaryUseCase } from '@core/racing/application/use-cases/GetLeagueOwnerSummaryUseCase'; import { GetLeagueProtestsUseCase } from '@core/racing/application/use-cases/GetLeagueProtestsUseCase'; import type { Logger } from '@core/shared/application/Logger'; +import { Result } from '@core/shared/application/Result'; describe('LeagueService', () => { let service: LeagueService; @@ -60,9 +61,7 @@ describe('LeagueService', () => { }); it('should get total leagues', async () => { - mockGetTotalLeaguesUseCase.execute.mockImplementation(async (params, presenter) => { - presenter.present({ totalLeagues: 5 }); - }); + mockGetTotalLeaguesUseCase.execute.mockResolvedValue(Result.ok({ totalLeagues: 5 })); const result = await service.getTotalLeagues(); diff --git a/apps/api/src/domain/league/LeagueService.ts b/apps/api/src/domain/league/LeagueService.ts index 0b336420b..7c2656c02 100644 --- a/apps/api/src/domain/league/LeagueService.ts +++ b/apps/api/src/domain/league/LeagueService.ts @@ -10,30 +10,27 @@ import { RejectJoinRequestInputDTO } from './dtos/RejectJoinRequestInputDTO'; import { RemoveLeagueMemberInputDTO } from './dtos/RemoveLeagueMemberInputDTO'; import { UpdateLeagueMemberRoleInputDTO } from './dtos/UpdateLeagueMemberRoleInputDTO'; import { LeagueAdminDTO } from './dtos/LeagueAdminDTO'; +import { LeagueAdminPermissionsDTO } from './dtos/LeagueAdminPermissionsDTO'; import { LeagueAdminProtestsDTO } from './dtos/LeagueAdminProtestsDTO'; import { LeagueMembershipsDTO } from './dtos/LeagueMembershipsDTO'; import { LeagueSeasonSummaryDTO } from './dtos/LeagueSeasonSummaryDTO'; import { GetSeasonSponsorshipsOutputDTO } from './dtos/GetSeasonSponsorshipsOutputDTO'; import { GetLeagueRacesOutputDTO } from './dtos/GetLeagueRacesOutputDTO'; +import { RejectJoinRequestOutputDTO } from './dtos/RejectJoinRequestOutputDTO'; +import { RemoveLeagueMemberOutputDTO } from './dtos/RemoveLeagueMemberOutputDTO'; +import { TransferLeagueOwnershipOutputDTO } from './dtos/TransferLeagueOwnershipOutputDTO'; +import { UpdateLeagueMemberRoleOutputDTO } from './dtos/UpdateLeagueMemberRoleOutputDTO'; // Core imports for view models -import type { LeagueScoringConfigViewModel } from '@core/racing/application/presenters/ILeagueScoringConfigPresenter'; -import type { LeagueScoringPresetsViewModel } from '@core/racing/application/presenters/ILeagueScoringPresetsPresenter'; -import type { AllLeaguesWithCapacityViewModel } from '@core/racing/application/presenters/IAllLeaguesWithCapacityPresenter'; -import type { GetTotalLeaguesViewModel } from '@core/racing/application/presenters/IGetTotalLeaguesPresenter'; +import type { LeagueScoringConfigViewModel } from './presenters/LeagueScoringConfigPresenter'; +import type { LeagueScoringPresetsViewModel } from './presenters/LeagueScoringPresetsPresenter'; +import type { AllLeaguesWithCapacityDTO as AllLeaguesWithCapacityViewModel } from '../dtos/AllLeaguesWithCapacityDTO'; import type { GetLeagueJoinRequestsViewModel } from '@core/racing/application/presenters/IGetLeagueJoinRequestsPresenter'; -import type { ApproveLeagueJoinRequestViewModel } from '@core/racing/application/presenters/IApproveLeagueJoinRequestPresenter'; -import type { RejectLeagueJoinRequestViewModel } from '@core/racing/application/presenters/IRejectLeagueJoinRequestPresenter'; +import { TotalLeaguesDTO } from './dtos/TotalLeaguesDTO'; +import type { ApproveLeagueJoinRequestDTO } from './dtos/ApproveLeagueJoinRequestDTO'; +import type { JoinLeagueOutputDTO } from './dtos/JoinLeagueOutputDTO'; import type { GetLeagueAdminPermissionsViewModel } from '@core/racing/application/presenters/IGetLeagueAdminPermissionsPresenter'; -import type { RemoveLeagueMemberViewModel } from '@core/racing/application/presenters/IRemoveLeagueMemberPresenter'; -import type { UpdateLeagueMemberRoleViewModel } from '@core/racing/application/presenters/IUpdateLeagueMemberRolePresenter'; -import type { GetLeagueOwnerSummaryViewModel } from '@core/racing/application/presenters/IGetLeagueOwnerSummaryPresenter'; -import type { LeagueStandingsViewModel } from '@core/racing/application/presenters/ILeagueStandingsPresenter'; -import type { LeagueStatsViewModel } from '@core/racing/application/presenters/ILeagueStatsPresenter'; -import type { LeagueConfigFormViewModel } from '@core/racing/application/presenters/ILeagueFullConfigPresenter'; -import type { CreateLeagueViewModel } from '@core/racing/application/presenters/ICreateLeaguePresenter'; -import type { JoinLeagueViewModel } from '@core/racing/application/presenters/IJoinLeaguePresenter'; -import type { TransferLeagueOwnershipViewModel } from '@core/racing/application/presenters/ITransferLeagueOwnershipPresenter'; +import type { CreateLeagueViewModel } from './dtos/CreateLeagueDTO'; // Core imports import type { Logger } from '@core/shared/application/Logger'; @@ -67,22 +64,22 @@ import { AllLeaguesWithCapacityPresenter } from './presenters/AllLeaguesWithCapa import { TotalLeaguesPresenter } from './presenters/TotalLeaguesPresenter'; import { LeagueScoringConfigPresenter } from './presenters/LeagueScoringConfigPresenter'; import { LeagueScoringPresetsPresenter } from './presenters/LeagueScoringPresetsPresenter'; -import { ApproveLeagueJoinRequestPresenter } from './presenters/ApproveLeagueJoinRequestPresenter'; +import { mapApproveLeagueJoinRequestPortToDTO } from './presenters/ApproveLeagueJoinRequestPresenter'; import { GetLeagueAdminPermissionsPresenter } from './presenters/GetLeagueAdminPermissionsPresenter'; -import { GetLeagueOwnerSummaryPresenter } from './presenters/GetLeagueOwnerSummaryPresenter'; +import { mapGetLeagueOwnerSummaryOutputPortToDTO } from './presenters/GetLeagueOwnerSummaryPresenter'; import { LeagueJoinRequestsPresenter } from './presenters/LeagueJoinRequestsPresenter'; -import { LeagueSchedulePresenter } from './presenters/LeagueSchedulePresenter'; +import { mapGetLeagueScheduleOutputPortToDTO } from './presenters/LeagueSchedulePresenter'; import { LeagueStatsPresenter } from './presenters/LeagueStatsPresenter'; -import { RejectLeagueJoinRequestPresenter } from './presenters/RejectLeagueJoinRequestPresenter'; -import { RemoveLeagueMemberPresenter } from './presenters/RemoveLeagueMemberPresenter'; -import { UpdateLeagueMemberRolePresenter } from './presenters/UpdateLeagueMemberRolePresenter'; +import { mapRejectLeagueJoinRequestOutputPortToDTO } from './presenters/RejectLeagueJoinRequestPresenter'; +import { mapRemoveLeagueMemberOutputPortToDTO } from './presenters/RemoveLeagueMemberPresenter'; +import { mapUpdateLeagueMemberRoleOutputPortToDTO } from './presenters/UpdateLeagueMemberRolePresenter'; import { CreateLeaguePresenter } from './presenters/CreateLeaguePresenter'; -import { JoinLeaguePresenter } from './presenters/JoinLeaguePresenter'; -import { TransferLeagueOwnershipPresenter } from './presenters/TransferLeagueOwnershipPresenter'; -import { GetLeagueProtestsPresenter } from './presenters/GetLeagueProtestsPresenter'; -import { GetLeagueSeasonsPresenter } from './presenters/GetLeagueSeasonsPresenter'; -import { GetLeagueMembershipsPresenter } from './presenters/GetLeagueMembershipsPresenter'; - +import { mapJoinLeagueOutputPortToDTO } from './presenters/JoinLeaguePresenter'; +import { mapTransferLeagueOwnershipOutputPortToDTO } from './presenters/TransferLeagueOwnershipPresenter'; +import { mapGetLeagueProtestsOutputPortToDTO } from './presenters/GetLeagueProtestsPresenter'; +import { mapGetLeagueSeasonsOutputPortToDTO } from './presenters/GetLeagueSeasonsPresenter'; +import { LeagueConfigPresenter } from './presenters/LeagueConfigPresenter'; +import { LeagueStandingsPresenter } from './presenters/LeagueStandingsPresenter'; // Tokens import { LOGGER_TOKEN } from './LeagueProviders'; @@ -126,7 +123,7 @@ export class LeagueService { return presenter.getViewModel()!; } - async getTotalLeagues(): Promise { + async getTotalLeagues(): Promise { this.logger.debug('[LeagueService] Fetching total leagues count.'); const result = await this.getTotalLeaguesUseCase.execute(); if (result.isErr()) { @@ -148,26 +145,22 @@ export class LeagueService { return presenter.getViewModel(); } - async approveLeagueJoinRequest(input: ApproveJoinRequestInputDTO): Promise { + async approveLeagueJoinRequest(input: ApproveJoinRequestInputDTO): Promise { this.logger.debug('Approving join request:', input); const result = await this.approveLeagueJoinRequestUseCase.execute({ leagueId: input.leagueId, requestId: input.requestId }); if (result.isErr()) { throw new Error(result.unwrapErr().code); } - const presenter = new ApproveLeagueJoinRequestPresenter(); - presenter.present(result.unwrap()); - return presenter.getViewModel(); + return mapApproveLeagueJoinRequestPortToDTO(result.unwrap()); } - async rejectLeagueJoinRequest(input: RejectJoinRequestInputDTO): Promise { + async rejectLeagueJoinRequest(input: RejectJoinRequestInputDTO): Promise { this.logger.debug('Rejecting join request:', input); const result = await this.rejectLeagueJoinRequestUseCase.execute({ requestId: input.requestId }); if (result.isErr()) { throw new Error(result.unwrapErr().code); } - const presenter = new RejectLeagueJoinRequestPresenter(); - presenter.present(result.unwrap()); - return presenter.getViewModel(); + return mapRejectLeagueJoinRequestOutputPortToDTO(result.unwrap()); } async getLeagueAdminPermissions(query: GetLeagueAdminPermissionsInputDTO): Promise { @@ -179,40 +172,34 @@ export class LeagueService { return presenter.getViewModel()!; } - async removeLeagueMember(input: RemoveLeagueMemberInputDTO): Promise { + async removeLeagueMember(input: RemoveLeagueMemberInputDTO): Promise { this.logger.debug('Removing league member', { leagueId: input.leagueId, targetDriverId: input.targetDriverId }); const result = await this.removeLeagueMemberUseCase.execute({ leagueId: input.leagueId, targetDriverId: input.targetDriverId }); if (result.isErr()) { throw new Error(result.unwrapErr().code); } - const presenter = new RemoveLeagueMemberPresenter(); - presenter.present(result.unwrap()); - return presenter.getViewModel(); + return mapRemoveLeagueMemberOutputPortToDTO(result.unwrap()); } - async updateLeagueMemberRole(input: UpdateLeagueMemberRoleInputDTO): Promise { + async updateLeagueMemberRole(input: UpdateLeagueMemberRoleInputDTO): Promise { this.logger.debug('Updating league member role', { leagueId: input.leagueId, targetDriverId: input.targetDriverId, newRole: input.newRole }); const result = await this.updateLeagueMemberRoleUseCase.execute({ leagueId: input.leagueId, targetDriverId: input.targetDriverId, newRole: input.newRole }); if (result.isErr()) { throw new Error(result.unwrapErr().code); } - const presenter = new UpdateLeagueMemberRolePresenter(); - presenter.present(result.unwrap()); - return presenter.getViewModel(); + return mapUpdateLeagueMemberRoleOutputPortToDTO(result.unwrap()); } - async getLeagueOwnerSummary(query: GetLeagueOwnerSummaryQueryDTO): Promise { + async getLeagueOwnerSummary(query: GetLeagueOwnerSummaryQueryDTO): Promise { this.logger.debug('Getting league owner summary:', query); const result = await this.getLeagueOwnerSummaryUseCase.execute({ ownerId: query.ownerId }); if (result.isErr()) { throw new Error(result.unwrapErr().code); } - const presenter = new GetLeagueOwnerSummaryPresenter(); - presenter.present(result.unwrap()); - return presenter.getViewModel(); + return mapGetLeagueOwnerSummaryOutputPortToDTO(result.unwrap()); } - async getLeagueFullConfig(query: GetLeagueAdminConfigQueryDTO): Promise { + async getLeagueFullConfig(query: GetLeagueAdminConfigQueryDTO): Promise { this.logger.debug('Getting league full config', { query }); try { @@ -221,7 +208,9 @@ export class LeagueService { this.logger.error('Error getting league full config', new Error(result.unwrapErr().code)); return null; } - return result.unwrap(); + const presenter = new LeagueConfigPresenter(); + presenter.present(result.unwrap()); + return presenter.getViewModel(); } catch (error) { this.logger.error('Error getting league full config', error instanceof Error ? error : new Error(String(error))); return null; @@ -234,9 +223,7 @@ export class LeagueService { if (result.isErr()) { throw new Error(result.unwrapErr().code); } - const presenter = new GetLeagueProtestsPresenter(); - presenter.present(result.unwrap()); - return presenter.getViewModel() as LeagueAdminProtestsDTO; + return mapGetLeagueProtestsOutputPortToDTO(result.unwrap()); } async getLeagueSeasons(query: GetLeagueSeasonsQueryDTO): Promise { @@ -245,9 +232,7 @@ export class LeagueService { if (result.isErr()) { throw new Error(result.unwrapErr().code); } - const presenter = new GetLeagueSeasonsPresenter(); - presenter.present(result.unwrap()); - return presenter.getViewModel().seasons; + return mapGetLeagueSeasonsOutputPortToDTO(result.unwrap()); } async getLeagueMemberships(leagueId: string): Promise { @@ -261,25 +246,27 @@ export class LeagueService { return presenter.getViewModel().memberships as LeagueMembershipsDTO; } - async getLeagueStandings(leagueId: string): Promise { + async getLeagueStandings(leagueId: string): Promise { this.logger.debug('Getting league standings', { leagueId }); - const result = await this.getLeagueStandingsUseCase.execute(leagueId); - // The use case returns a view model directly, so we return it as-is - return result as unknown as LeagueStandingsViewModel; + const result = await this.getLeagueStandingsUseCase.execute({ leagueId }); + if (result.isErr()) { + throw new Error(result.unwrapErr().code); + } + const presenter = new LeagueStandingsPresenter(); + presenter.present(result.unwrap()); + return presenter.getViewModel(); } - async getLeagueSchedule(leagueId: string): Promise> { + async getLeagueSchedule(leagueId: string): Promise { this.logger.debug('Getting league schedule', { leagueId }); const result = await this.getLeagueScheduleUseCase.execute({ leagueId }); if (result.isErr()) { throw new Error(result.unwrapErr().code); } - const presenter = new LeagueSchedulePresenter(); - presenter.present(result.unwrap()); - return presenter.getViewModel()!; + return mapGetLeagueScheduleOutputPortToDTO(result.unwrap()); } - async getLeagueStats(leagueId: string): Promise { + async getLeagueStats(leagueId: string): Promise { this.logger.debug('Getting league stats', { leagueId }); const result = await this.getLeagueStatsUseCase.execute({ leagueId }); if (result.isErr()) { @@ -404,7 +391,7 @@ export class LeagueService { return presenter.getViewModel()!; } - async joinLeague(leagueId: string, driverId: string): Promise { + async joinLeague(leagueId: string, driverId: string): Promise { this.logger.debug('Joining league', { leagueId, driverId }); const result = await this.joinLeagueUseCase.execute({ leagueId, driverId }); @@ -415,12 +402,10 @@ export class LeagueService { error: error.code, }; } - const presenter = new JoinLeaguePresenter(); - presenter.present(result.unwrap()); - return presenter.getViewModel(); + return mapJoinLeagueOutputPortToDTO(result.unwrap()); } - async transferLeagueOwnership(leagueId: string, currentOwnerId: string, newOwnerId: string): Promise { + async transferLeagueOwnership(leagueId: string, currentOwnerId: string, newOwnerId: string): Promise { this.logger.debug('Transferring league ownership', { leagueId, currentOwnerId, newOwnerId }); const result = await this.transferLeagueOwnershipUseCase.execute({ leagueId, currentOwnerId, newOwnerId }); @@ -431,9 +416,7 @@ export class LeagueService { error: error.code, }; } - const presenter = new TransferLeagueOwnershipPresenter(); - presenter.present({ success: true }); - return presenter.getViewModel(); + return mapTransferLeagueOwnershipOutputPortToDTO(result.unwrap()); } async getSeasonSponsorships(seasonId: string): Promise { diff --git a/apps/api/src/domain/league/dtos/AllLeaguesWithCapacityDTO.ts b/apps/api/src/domain/league/dtos/AllLeaguesWithCapacityDTO.ts index 9d9b9a64a..2a5bc0b7c 100644 --- a/apps/api/src/domain/league/dtos/AllLeaguesWithCapacityDTO.ts +++ b/apps/api/src/domain/league/dtos/AllLeaguesWithCapacityDTO.ts @@ -1,16 +1,43 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNumber, IsArray, ValidateNested } from 'class-validator'; -import { Type } from 'class-transformer'; -import { LeagueWithCapacityDTO } from './LeagueWithCapacityDTO'; + +export class LeagueWithCapacityDTO { + @ApiProperty() + id!: string; + + @ApiProperty() + name!: string; + + @ApiProperty() + description!: string; + + @ApiProperty() + ownerId!: string; + + @ApiProperty() + settings!: { + maxDrivers: number; + sessionDuration?: number; + visibility?: string; + }; + + @ApiProperty() + createdAt!: string; + + @ApiProperty({ nullable: true }) + socialLinks?: { + discordUrl?: string; + youtubeUrl?: string; + websiteUrl?: string; + }; + + @ApiProperty() + usedSlots!: number; +} export class AllLeaguesWithCapacityDTO { @ApiProperty({ type: [LeagueWithCapacityDTO] }) - @IsArray() - @ValidateNested({ each: true }) - @Type(() => LeagueWithCapacityDTO) - leagues: LeagueWithCapacityDTO[]; + leagues!: LeagueWithCapacityDTO[]; @ApiProperty() - @IsNumber() - totalCount: number; + totalCount!: number; } \ No newline at end of file diff --git a/apps/api/src/domain/league/dtos/ApproveLeagueJoinRequestDTO.ts b/apps/api/src/domain/league/dtos/ApproveLeagueJoinRequestDTO.ts new file mode 100644 index 000000000..17de3e8aa --- /dev/null +++ b/apps/api/src/domain/league/dtos/ApproveLeagueJoinRequestDTO.ts @@ -0,0 +1,4 @@ +export interface ApproveLeagueJoinRequestDTO { + success: boolean; + message: string; +} \ No newline at end of file diff --git a/apps/api/src/domain/league/dtos/CreateLeagueDTO.ts b/apps/api/src/domain/league/dtos/CreateLeagueDTO.ts new file mode 100644 index 000000000..a665708b9 --- /dev/null +++ b/apps/api/src/domain/league/dtos/CreateLeagueDTO.ts @@ -0,0 +1,4 @@ +export interface CreateLeagueViewModel { + leagueId: string; + success: boolean; +} \ No newline at end of file diff --git a/apps/api/src/domain/league/dtos/TotalLeaguesDTO.ts b/apps/api/src/domain/league/dtos/TotalLeaguesDTO.ts new file mode 100644 index 000000000..07535fc0c --- /dev/null +++ b/apps/api/src/domain/league/dtos/TotalLeaguesDTO.ts @@ -0,0 +1,8 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNumber } from 'class-validator'; + +export class TotalLeaguesDTO { + @ApiProperty() + @IsNumber() + totalLeagues: number; +} \ No newline at end of file diff --git a/apps/api/src/domain/league/presenters/AllLeaguesWithCapacityPresenter.ts b/apps/api/src/domain/league/presenters/AllLeaguesWithCapacityPresenter.ts index 272834920..b16f3f94a 100644 --- a/apps/api/src/domain/league/presenters/AllLeaguesWithCapacityPresenter.ts +++ b/apps/api/src/domain/league/presenters/AllLeaguesWithCapacityPresenter.ts @@ -1,21 +1,22 @@ -import { IAllLeaguesWithCapacityPresenter, AllLeaguesWithCapacityResultDTO, AllLeaguesWithCapacityViewModel } from '@core/racing/application/presenters/IAllLeaguesWithCapacityPresenter'; +import type { AllLeaguesWithCapacityOutputPort } from '@core/racing/application/ports/output/AllLeaguesWithCapacityOutputPort'; +import { AllLeaguesWithCapacityDTO, LeagueWithCapacityDTO } from '../dtos/AllLeaguesWithCapacityDTO'; -export class AllLeaguesWithCapacityPresenter implements IAllLeaguesWithCapacityPresenter { - private result: AllLeaguesWithCapacityViewModel | null = null; +export class AllLeaguesWithCapacityPresenter { + private result: AllLeaguesWithCapacityDTO | null = null; reset() { this.result = null; } - present(dto: AllLeaguesWithCapacityResultDTO) { - const leagues = dto.leagues.map(league => ({ + present(output: AllLeaguesWithCapacityOutputPort) { + const leagues: LeagueWithCapacityDTO[] = output.leagues.map(league => ({ id: league.id, name: league.name, description: league.description, ownerId: league.ownerId, settings: { maxDrivers: league.settings.maxDrivers || 0 }, createdAt: league.createdAt.toISOString(), - usedSlots: dto.memberCounts.get(league.id) || 0, + usedSlots: output.memberCounts[league.id] || 0, socialLinks: league.socialLinks, })); this.result = { @@ -24,7 +25,7 @@ export class AllLeaguesWithCapacityPresenter implements IAllLeaguesWithCapacityP }; } - getViewModel(): AllLeaguesWithCapacityViewModel | null { + getViewModel(): AllLeaguesWithCapacityDTO | null { return this.result; } } \ No newline at end of file diff --git a/apps/api/src/domain/league/presenters/ApproveLeagueJoinRequestPresenter.ts b/apps/api/src/domain/league/presenters/ApproveLeagueJoinRequestPresenter.ts index f66a39559..4e09f124f 100644 --- a/apps/api/src/domain/league/presenters/ApproveLeagueJoinRequestPresenter.ts +++ b/apps/api/src/domain/league/presenters/ApproveLeagueJoinRequestPresenter.ts @@ -1,18 +1,6 @@ -import { IApproveLeagueJoinRequestPresenter, ApproveLeagueJoinRequestResultPort, ApproveLeagueJoinRequestViewModel } from '@core/racing/application/presenters/IApproveLeagueJoinRequestPresenter'; +import type { ApproveLeagueJoinRequestResultPort } from '@core/racing/application/ports/output/ApproveLeagueJoinRequestResultPort'; +import type { ApproveLeagueJoinRequestDTO } from '../dtos/ApproveLeagueJoinRequestDTO'; -export class ApproveLeagueJoinRequestPresenter implements IApproveLeagueJoinRequestPresenter { - private result: ApproveLeagueJoinRequestViewModel | null = null; - - reset() { - this.result = null; - } - - present(dto: ApproveLeagueJoinRequestResultPort) { - this.result = dto; - } - - getViewModel(): ApproveLeagueJoinRequestViewModel { - if (!this.result) throw new Error('Presenter not presented'); - return this.result; - } +export function mapApproveLeagueJoinRequestPortToDTO(port: ApproveLeagueJoinRequestResultPort): ApproveLeagueJoinRequestDTO { + return port; } \ No newline at end of file diff --git a/apps/api/src/domain/league/presenters/CreateLeaguePresenter.ts b/apps/api/src/domain/league/presenters/CreateLeaguePresenter.ts index 98fc2313c..46a15467a 100644 --- a/apps/api/src/domain/league/presenters/CreateLeaguePresenter.ts +++ b/apps/api/src/domain/league/presenters/CreateLeaguePresenter.ts @@ -1,13 +1,14 @@ -import { ICreateLeaguePresenter, CreateLeagueResultDTO, CreateLeagueViewModel } from '@core/racing/application/presenters/ICreateLeaguePresenter'; +import type { CreateLeagueWithSeasonAndScoringOutputPort } from '@core/racing/application/ports/output/CreateLeagueWithSeasonAndScoringOutputPort'; +import type { CreateLeagueViewModel } from '../dtos/CreateLeagueDTO'; -export class CreateLeaguePresenter implements ICreateLeaguePresenter { +export class CreateLeaguePresenter { private result: CreateLeagueViewModel | null = null; reset() { this.result = null; } - present(dto: CreateLeagueResultDTO): void { + present(dto: CreateLeagueWithSeasonAndScoringOutputPort): void { this.result = { leagueId: dto.leagueId, success: true, diff --git a/apps/api/src/domain/league/presenters/GetLeagueAdminPermissionsPresenter.ts b/apps/api/src/domain/league/presenters/GetLeagueAdminPermissionsPresenter.ts deleted file mode 100644 index df2a8105c..000000000 --- a/apps/api/src/domain/league/presenters/GetLeagueAdminPermissionsPresenter.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { IGetLeagueAdminPermissionsPresenter, GetLeagueAdminPermissionsResultDTO, GetLeagueAdminPermissionsViewModel } from '@core/racing/application/presenters/IGetLeagueAdminPermissionsPresenter'; - -export class GetLeagueAdminPermissionsPresenter implements IGetLeagueAdminPermissionsPresenter { - private result: GetLeagueAdminPermissionsViewModel | null = null; - - reset() { - this.result = null; - } - - present(dto: GetLeagueAdminPermissionsResultDTO) { - this.result = dto; - } - - getViewModel(): GetLeagueAdminPermissionsViewModel | null { - return this.result; - } -} \ No newline at end of file diff --git a/apps/api/src/domain/league/presenters/GetLeagueMembershipsPresenter.ts b/apps/api/src/domain/league/presenters/GetLeagueMembershipsPresenter.ts deleted file mode 100644 index de4f6747f..000000000 --- a/apps/api/src/domain/league/presenters/GetLeagueMembershipsPresenter.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { IGetLeagueMembershipsPresenter, GetLeagueMembershipsResultDTO, GetLeagueMembershipsViewModel } from '@core/racing/application/presenters/IGetLeagueMembershipsPresenter'; -import { LeagueMembershipsDTO } from '../dtos/LeagueMembershipsDTO'; - -export class GetLeagueMembershipsPresenter implements IGetLeagueMembershipsPresenter { - private result: GetLeagueMembershipsViewModel | null = null; - - reset() { - this.result = null; - } - - present(dto: GetLeagueMembershipsResultDTO) { - const driverMap = new Map(dto.drivers.map(d => [d.id, d])); - const members = dto.memberships.map(membership => ({ - driverId: membership.driverId, - driver: driverMap.get(membership.driverId)!, - role: this.mapRole(membership.role) as 'owner' | 'manager' | 'member', - joinedAt: membership.joinedAt, - })); - this.result = { memberships: { members } }; - } - - private mapRole(role: string): 'owner' | 'manager' | 'member' { - switch (role) { - case 'owner': - return 'owner'; - case 'admin': - return 'manager'; // Map admin to manager for API - case 'steward': - return 'member'; // Map steward to member for API - case 'member': - return 'member'; - default: - return 'member'; - } - } - - getViewModel(): GetLeagueMembershipsViewModel { - if (!this.result) throw new Error('Presenter not presented'); - return this.result; - } - - // API-specific method - get apiViewModel(): LeagueMembershipsDTO | null { - if (!this.result?.memberships) return null; - return this.result.memberships as LeagueMembershipsViewModel; - } -} \ No newline at end of file diff --git a/apps/api/src/domain/league/presenters/GetLeagueOwnerSummaryPresenter.ts b/apps/api/src/domain/league/presenters/GetLeagueOwnerSummaryPresenter.ts index 0d56b0a8d..ad7f06a7c 100644 --- a/apps/api/src/domain/league/presenters/GetLeagueOwnerSummaryPresenter.ts +++ b/apps/api/src/domain/league/presenters/GetLeagueOwnerSummaryPresenter.ts @@ -1,18 +1,19 @@ -import { IGetLeagueOwnerSummaryPresenter, GetLeagueOwnerSummaryResultDTO, GetLeagueOwnerSummaryViewModel } from '@core/racing/application/presenters/IGetLeagueOwnerSummaryPresenter'; +import { GetLeagueOwnerSummaryOutputPort } from '@core/racing/application/ports/output/GetLeagueOwnerSummaryOutputPort'; +import { LeagueOwnerSummaryDTO } from '../dtos/LeagueOwnerSummaryDTO'; -export class GetLeagueOwnerSummaryPresenter implements IGetLeagueOwnerSummaryPresenter { - private result: GetLeagueOwnerSummaryViewModel | null = null; +export function mapGetLeagueOwnerSummaryOutputPortToDTO(output: GetLeagueOwnerSummaryOutputPort): LeagueOwnerSummaryDTO | null { + if (!output.summary) return null; - reset() { - this.result = null; - } - - present(dto: GetLeagueOwnerSummaryResultDTO) { - this.result = { summary: dto.summary }; - } - - getViewModel(): GetLeagueOwnerSummaryViewModel { - if (!this.result) throw new Error('Presenter not presented'); - return this.result; - } + return { + driver: { + id: output.summary.driver.id, + iracingId: output.summary.driver.iracingId, + name: output.summary.driver.name, + country: output.summary.driver.country, + bio: output.summary.driver.bio, + joinedAt: output.summary.driver.joinedAt, + }, + rating: output.summary.rating, + rank: output.summary.rank, + }; } \ No newline at end of file diff --git a/apps/api/src/domain/league/presenters/GetLeagueProtestsPresenter.ts b/apps/api/src/domain/league/presenters/GetLeagueProtestsPresenter.ts index c5f873db5..c68759b58 100644 --- a/apps/api/src/domain/league/presenters/GetLeagueProtestsPresenter.ts +++ b/apps/api/src/domain/league/presenters/GetLeagueProtestsPresenter.ts @@ -1,30 +1,47 @@ -import { IGetLeagueProtestsPresenter, GetLeagueProtestsResultDTO, GetLeagueProtestsViewModel } from '@core/racing/application/presenters/IGetLeagueProtestsPresenter'; +import { GetLeagueProtestsOutputPort } from '@core/racing/application/ports/output/GetLeagueProtestsOutputPort'; +import { LeagueAdminProtestsDTO } from '../dtos/LeagueAdminProtestsDTO'; +import { ProtestDTO } from '../dtos/ProtestDTO'; +import { RaceDTO } from '../../race/dtos/RaceDTO'; +import { DriverDTO } from '../../driver/dtos/DriverDTO'; -export class GetLeagueProtestsPresenter implements IGetLeagueProtestsPresenter { - private result: GetLeagueProtestsViewModel | null = null; +export function mapGetLeagueProtestsOutputPortToDTO(output: GetLeagueProtestsOutputPort): LeagueAdminProtestsDTO { + const protests: ProtestDTO[] = output.protests.map(protest => ({ + id: protest.id, + raceId: protest.raceId, + protestingDriverId: protest.protestingDriverId, + accusedDriverId: protest.accusedDriverId, + submittedAt: new Date(protest.filedAt), + description: protest.incident.description, + status: protest.status as 'pending' | 'accepted' | 'rejected', // TODO: map properly + })); - reset() { - this.result = null; - } - - present(dto: GetLeagueProtestsResultDTO) { - const racesById = {}; - dto.races.forEach(race => { - racesById[race.id] = race; - }); - const driversById = {}; - dto.drivers.forEach(driver => { - driversById[driver.id] = driver; - }); - this.result = { - protests: dto.protests, - racesById, - driversById, + const racesById: { [raceId: string]: RaceDTO } = {}; + for (const raceId in output.racesById) { + const race = output.racesById[raceId]; + racesById[raceId] = { + id: race.id, + name: race.track, // assuming name is track + date: race.scheduledAt, + leagueName: undefined, // TODO: get league name if needed }; } - getViewModel(): GetLeagueProtestsViewModel { - if (!this.result) throw new Error('Presenter not presented'); - return this.result; + const driversById: { [driverId: string]: DriverDTO } = {}; + for (const driverId in output.driversById) { + const driver = output.driversById[driverId]; + driversById[driverId] = { + id: driver.id, + iracingId: driver.iracingId, + name: driver.name, + country: driver.country, + bio: driver.bio, + joinedAt: driver.joinedAt, + }; } + + return { + protests, + racesById, + driversById, + }; } \ No newline at end of file diff --git a/apps/api/src/domain/league/presenters/GetLeagueSeasonsPresenter.ts b/apps/api/src/domain/league/presenters/GetLeagueSeasonsPresenter.ts index 442505586..21d586fc0 100644 --- a/apps/api/src/domain/league/presenters/GetLeagueSeasonsPresenter.ts +++ b/apps/api/src/domain/league/presenters/GetLeagueSeasonsPresenter.ts @@ -1,27 +1,14 @@ -import { IGetLeagueSeasonsPresenter, GetLeagueSeasonsResultDTO, GetLeagueSeasonsViewModel } from '@core/racing/application/presenters/IGetLeagueSeasonsPresenter'; +import { GetLeagueSeasonsOutputPort } from '@core/racing/application/ports/output/GetLeagueSeasonsOutputPort'; +import { LeagueSeasonSummaryDTO } from '../dtos/LeagueSeasonSummaryDTO'; -export class GetLeagueSeasonsPresenter implements IGetLeagueSeasonsPresenter { - private result: GetLeagueSeasonsViewModel | null = null; - - reset() { - this.result = null; - } - - present(dto: GetLeagueSeasonsResultDTO) { - const seasons = dto.seasons.map(season => ({ - seasonId: season.id, - name: season.name, - status: season.status, - startDate: season.startDate, - endDate: season.endDate, - isPrimary: season.isPrimary, - isParallelActive: season.isParallelActive, - })); - this.result = { seasons }; - } - - getViewModel(): GetLeagueSeasonsViewModel { - if (!this.result) throw new Error('Presenter not presented'); - return this.result; - } +export function mapGetLeagueSeasonsOutputPortToDTO(output: GetLeagueSeasonsOutputPort): LeagueSeasonSummaryDTO[] { + return output.seasons.map(season => ({ + seasonId: season.seasonId, + name: season.name, + status: season.status, + startDate: season.startDate, + endDate: season.endDate, + isPrimary: season.isPrimary, + isParallelActive: season.isParallelActive, + })); } \ No newline at end of file diff --git a/apps/api/src/domain/league/presenters/JoinLeaguePresenter.ts b/apps/api/src/domain/league/presenters/JoinLeaguePresenter.ts index 8468b7910..6bd6fe1c6 100644 --- a/apps/api/src/domain/league/presenters/JoinLeaguePresenter.ts +++ b/apps/api/src/domain/league/presenters/JoinLeaguePresenter.ts @@ -1,21 +1,9 @@ -import { IJoinLeaguePresenter, JoinLeagueResultDTO, JoinLeagueViewModel } from '@core/racing/application/presenters/IJoinLeaguePresenter'; +import type { JoinLeagueOutputPort } from '@core/racing/application/ports/output/JoinLeagueOutputPort'; +import type { JoinLeagueOutputDTO } from '../dtos/JoinLeagueOutputDTO'; -export class JoinLeaguePresenter implements IJoinLeaguePresenter { - private result: JoinLeagueViewModel | null = null; - - reset() { - this.result = null; - } - - present(dto: JoinLeagueResultDTO): void { - this.result = { - success: true, - membershipId: dto.id, - }; - } - - getViewModel(): JoinLeagueViewModel { - if (!this.result) throw new Error('Presenter not presented'); - return this.result; - } +export function mapJoinLeagueOutputPortToDTO(port: JoinLeagueOutputPort): JoinLeagueOutputDTO { + return { + success: true, + membershipId: port.membershipId, + }; } \ No newline at end of file diff --git a/apps/api/src/domain/league/presenters/LeagueConfigPresenter.ts b/apps/api/src/domain/league/presenters/LeagueConfigPresenter.ts index 19fa7f4e0..0ec0aa3f3 100644 --- a/apps/api/src/domain/league/presenters/LeagueConfigPresenter.ts +++ b/apps/api/src/domain/league/presenters/LeagueConfigPresenter.ts @@ -1,15 +1,16 @@ -import { ILeagueFullConfigPresenter, LeagueFullConfigData, LeagueConfigFormViewModel } from '@core/racing/application/presenters/ILeagueFullConfigPresenter'; +import { LeagueFullConfigOutputPort } from '@core/racing/application/ports/output/LeagueFullConfigOutputPort'; import { LeagueConfigFormModelDTO } from '../dtos/LeagueConfigFormModelDTO'; +import type { Presenter } from '@core/shared/presentation'; -export class LeagueConfigPresenter implements ILeagueFullConfigPresenter { - private result: LeagueConfigFormViewModel | null = null; +export class LeagueConfigPresenter implements Presenter { + private result: LeagueConfigFormModelDTO | null = null; reset() { this.result = null; } - present(dto: LeagueFullConfigData) { - // Map from LeagueFullConfigData to LeagueConfigFormViewModel + present(dto: LeagueFullConfigOutputPort) { + // Map from LeagueFullConfigOutputPort to LeagueConfigFormModelDTO const league = dto.league; const settings = league.settings; const stewarding = settings.stewarding; @@ -20,64 +21,9 @@ export class LeagueConfigPresenter implements ILeagueFullConfigPresenter { name: league.name, description: league.description, visibility: 'public', // TODO: Map visibility from league - gameId: 'iracing', // TODO: Map from game }, structure: { mode: 'solo', // TODO: Map from league settings - maxDrivers: settings.maxDrivers || 32, - multiClassEnabled: false, // TODO: Map - }, - championships: { - enableDriverChampionship: true, // TODO: Map - enableTeamChampionship: false, - enableNationsChampionship: false, - enableTrophyChampionship: false, - }, - scoring: { - customScoringEnabled: false, // TODO: Map - }, - dropPolicy: { - strategy: 'none', // TODO: Map - }, - timings: { - practiceMinutes: 30, // TODO: Map - qualifyingMinutes: 15, - mainRaceMinutes: 60, - sessionCount: 1, - roundsPlanned: 10, // TODO: Map - }, - stewarding: { - decisionMode: stewarding?.decisionMode || 'admin_only', - requireDefense: stewarding?.requireDefense || false, - defenseTimeLimit: stewarding?.defenseTimeLimit || 48, - voteTimeLimit: stewarding?.voteTimeLimit || 72, - protestDeadlineHours: stewarding?.protestDeadlineHours || 48, - stewardingClosesHours: stewarding?.stewardingClosesHours || 168, - notifyAccusedOnProtest: stewarding?.notifyAccusedOnProtest || true, - notifyOnVoteRequired: stewarding?.notifyOnVoteRequired || true, - requiredVotes: stewarding?.requiredVotes, - }, - }; - } - - getViewModel(): LeagueConfigFormViewModel | null { - return this.result; - } - - // API-specific method to get the DTO - get viewModel(): LeagueConfigFormModelDTO | null { - if (!this.result) return null; - - // Map from LeagueConfigFormViewModel to LeagueConfigFormModelDto - return { - leagueId: this.result.leagueId, - basics: { - name: this.result.basics.name, - description: this.result.basics.description, - visibility: this.result.basics.visibility as 'public' | 'private', - }, - structure: { - mode: this.result.structure.mode as 'solo' | 'team', }, championships: [], // TODO: Map championships scoring: { @@ -85,8 +31,8 @@ export class LeagueConfigPresenter implements ILeagueFullConfigPresenter { points: 25, // TODO: Map points }, dropPolicy: { - strategy: this.result.dropPolicy.strategy as 'none' | 'worst_n', - n: this.result.dropPolicy.n, + strategy: 'none', // TODO: Map + n: 0, }, timings: { raceDayOfWeek: 'sunday', // TODO: Map from timings @@ -94,16 +40,20 @@ export class LeagueConfigPresenter implements ILeagueFullConfigPresenter { raceTimeMinute: 0, }, stewarding: { - decisionMode: this.result.stewarding.decisionMode === 'steward_vote' ? 'committee_vote' : 'single_steward', - requireDefense: this.result.stewarding.requireDefense, - defenseTimeLimit: this.result.stewarding.defenseTimeLimit, - voteTimeLimit: this.result.stewarding.voteTimeLimit, - protestDeadlineHours: this.result.stewarding.protestDeadlineHours, - stewardingClosesHours: this.result.stewarding.stewardingClosesHours, - notifyAccusedOnProtest: this.result.stewarding.notifyAccusedOnProtest, - notifyOnVoteRequired: this.result.stewarding.notifyOnVoteRequired, - requiredVotes: this.result.stewarding.requiredVotes, + decisionMode: stewarding?.decisionMode === 'steward_vote' ? 'committee_vote' : 'single_steward', + requireDefense: stewarding?.requireDefense || false, + defenseTimeLimit: stewarding?.defenseTimeLimit || 48, + voteTimeLimit: stewarding?.voteTimeLimit || 72, + protestDeadlineHours: stewarding?.protestDeadlineHours || 48, + stewardingClosesHours: stewarding?.stewardingClosesHours || 168, + notifyAccusedOnProtest: stewarding?.notifyAccusedOnProtest || true, + notifyOnVoteRequired: stewarding?.notifyOnVoteRequired || true, + requiredVotes: stewarding?.requiredVotes || 0, }, }; } + + getViewModel(): LeagueConfigFormModelDTO | null { + return this.result; + } } \ No newline at end of file diff --git a/apps/api/src/domain/league/presenters/LeagueJoinRequestsPresenter.ts b/apps/api/src/domain/league/presenters/LeagueJoinRequestsPresenter.ts deleted file mode 100644 index 6ee5396a9..000000000 --- a/apps/api/src/domain/league/presenters/LeagueJoinRequestsPresenter.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { IGetLeagueJoinRequestsPresenter, GetLeagueJoinRequestsResultDTO, GetLeagueJoinRequestsViewModel } from '@core/racing/application/presenters/IGetLeagueJoinRequestsPresenter'; - -export class LeagueJoinRequestsPresenter implements IGetLeagueJoinRequestsPresenter { - private result: GetLeagueJoinRequestsViewModel | null = null; - - reset() { - this.result = null; - } - - present(dto: GetLeagueJoinRequestsResultDTO) { - const driverMap = new Map(dto.drivers.map(d => [d.id, d])); - const joinRequests = dto.joinRequests.map(request => ({ - id: request.id, - leagueId: request.leagueId, - driverId: request.driverId, - requestedAt: request.requestedAt, - message: request.message, - driver: driverMap.get(request.driverId) || null, - })); - this.result = { joinRequests }; - } - - getViewModel(): GetLeagueJoinRequestsViewModel { - if (!this.result) throw new Error('Presenter not presented'); - return this.result; - } -} \ No newline at end of file diff --git a/apps/api/src/domain/league/presenters/LeagueSchedulePresenter.ts b/apps/api/src/domain/league/presenters/LeagueSchedulePresenter.ts index a1d937d7a..49f6dee59 100644 --- a/apps/api/src/domain/league/presenters/LeagueSchedulePresenter.ts +++ b/apps/api/src/domain/league/presenters/LeagueSchedulePresenter.ts @@ -1,23 +1,14 @@ -import { IGetLeagueSchedulePresenter, GetLeagueScheduleResultDTO, LeagueScheduleViewModel } from '@core/racing/application/presenters/IGetLeagueSchedulePresenter'; +import { GetLeagueScheduleOutputPort } from '@core/racing/application/ports/output/GetLeagueScheduleOutputPort'; +import { LeagueScheduleDTO } from '../dtos/LeagueScheduleDTO'; +import { RaceDTO } from '../../race/dtos/RaceDTO'; -export class LeagueSchedulePresenter implements IGetLeagueSchedulePresenter { - private result: LeagueScheduleViewModel | null = null; - - reset() { - this.result = null; - } - - present(dto: GetLeagueScheduleResultDTO) { - this.result = { - races: dto.races.map(race => ({ - id: race.id, - name: race.name, - date: race.scheduledAt.toISOString(), - })), - }; - } - - getViewModel(): LeagueScheduleViewModel | null { - return this.result; - } +export function mapGetLeagueScheduleOutputPortToDTO(output: GetLeagueScheduleOutputPort): LeagueScheduleDTO { + return { + races: output.races.map(race => ({ + id: race.id, + name: race.name, + date: race.scheduledAt.toISOString(), + leagueName: undefined, // TODO: get league name if needed + })), + }; } \ No newline at end of file diff --git a/apps/api/src/domain/league/presenters/LeagueScoringConfigPresenter.ts b/apps/api/src/domain/league/presenters/LeagueScoringConfigPresenter.ts index b2bbb9a01..b375e13ee 100644 --- a/apps/api/src/domain/league/presenters/LeagueScoringConfigPresenter.ts +++ b/apps/api/src/domain/league/presenters/LeagueScoringConfigPresenter.ts @@ -1,20 +1,37 @@ import type { ChampionshipConfig } from '@core/racing/domain/types/ChampionshipConfig'; import type { BonusRule } from '@core/racing/domain/types/BonusRule'; -import type { - ILeagueScoringConfigPresenter, - LeagueScoringConfigData, - LeagueScoringConfigViewModel, - LeagueScoringChampionshipViewModel, -} from '@core/racing/application/presenters/ILeagueScoringConfigPresenter'; +import type { LeagueScoringConfigOutputPort } from '@core/racing/application/ports/output/LeagueScoringConfigOutputPort'; +import type { LeagueScoringPresetOutputPort } from '@core/racing/application/ports/output/LeagueScoringPresetOutputPort'; -export class LeagueScoringConfigPresenter implements ILeagueScoringConfigPresenter { +export interface LeagueScoringChampionshipViewModel { + id: string; + name: string; + type: string; + sessionTypes: string[]; + pointsPreview: Array<{ sessionType: string; position: number; points: number }>; + bonusSummary: string[]; + dropPolicyDescription: string; +} + +export interface LeagueScoringConfigViewModel { + leagueId: string; + seasonId: string; + gameId: string; + gameName: string; + scoringPresetId?: string; + scoringPresetName?: string; + dropPolicySummary: string; + championships: LeagueScoringChampionshipViewModel[]; +} + +export class LeagueScoringConfigPresenter { private viewModel: LeagueScoringConfigViewModel | null = null; reset(): void { this.viewModel = null; } - present(data: LeagueScoringConfigData): LeagueScoringConfigViewModel { + present(data: LeagueScoringConfigOutputPort): LeagueScoringConfigViewModel { const championships: LeagueScoringChampionshipViewModel[] = data.championships.map((champ) => this.mapChampionship(champ)); diff --git a/apps/api/src/domain/league/presenters/LeagueScoringPresetsPresenter.ts b/apps/api/src/domain/league/presenters/LeagueScoringPresetsPresenter.ts index 95893504c..2341a15a8 100644 --- a/apps/api/src/domain/league/presenters/LeagueScoringPresetsPresenter.ts +++ b/apps/api/src/domain/league/presenters/LeagueScoringPresetsPresenter.ts @@ -1,19 +1,22 @@ -import type { - ILeagueScoringPresetsPresenter, - LeagueScoringPresetsResultDTO, - LeagueScoringPresetsViewModel, -} from '@core/racing/application/presenters/ILeagueScoringPresetsPresenter'; +import type { LeagueScoringPresetsOutputPort } from '@core/racing/application/ports/output/LeagueScoringPresetsOutputPort'; +import type { LeagueScoringPresetOutputPort } from '@core/racing/application/ports/output/LeagueScoringPresetOutputPort'; -export class LeagueScoringPresetsPresenter implements ILeagueScoringPresetsPresenter { +export interface LeagueScoringPresetsViewModel { + presets: LeagueScoringPresetOutputPort[]; + totalCount: number; +} + +export class LeagueScoringPresetsPresenter { private viewModel: LeagueScoringPresetsViewModel | null = null; reset(): void { this.viewModel = null; } - present(dto: LeagueScoringPresetsResultDTO): void { + present(output: LeagueScoringPresetsOutputPort): void { this.viewModel = { - presets: dto.presets, + presets: output.presets, + totalCount: output.presets.length, }; } diff --git a/apps/api/src/domain/league/presenters/LeagueStandingsPresenter.ts b/apps/api/src/domain/league/presenters/LeagueStandingsPresenter.ts index 04aa147d9..35f1e1a1f 100644 --- a/apps/api/src/domain/league/presenters/LeagueStandingsPresenter.ts +++ b/apps/api/src/domain/league/presenters/LeagueStandingsPresenter.ts @@ -1,26 +1,29 @@ -import { ILeagueStandingsPresenter, LeagueStandingsResultDTO, LeagueStandingsViewModel } from '@core/racing/application/presenters/ILeagueStandingsPresenter'; +import { LeagueStandingsOutputPort } from '@core/racing/application/ports/output/LeagueStandingsOutputPort'; +import { LeagueStandingsDTO } from '../dtos/LeagueStandingsDTO'; +import type { Presenter } from '@core/shared/presentation'; -export class LeagueStandingsPresenter implements ILeagueStandingsPresenter { - private result: LeagueStandingsViewModel | null = null; +export class LeagueStandingsPresenter implements Presenter { + private result: LeagueStandingsDTO | null = null; reset() { this.result = null; } - present(dto: LeagueStandingsResultDTO) { - const driverMap = new Map(dto.drivers.map(d => [d.id, { id: d.id, name: d.name }])); - const standings = dto.standings - .sort((a, b) => a.position - b.position) - .map(standing => ({ - driverId: standing.driverId, - driver: driverMap.get(standing.driverId)!, - points: standing.points, - rank: standing.position, - })); + present(dto: LeagueStandingsOutputPort) { + const standings = dto.standings.map(standing => ({ + driverId: standing.driverId, + driver: { + id: standing.driver.id, + name: standing.driver.name, + // Add other DriverDto fields if needed, but for now just id and name + }, + points: standing.points, + rank: standing.rank, + })); this.result = { standings }; } - getViewModel(): LeagueStandingsViewModel { + getViewModel(): LeagueStandingsDTO { if (!this.result) throw new Error('Presenter not presented'); return this.result; } diff --git a/apps/api/src/domain/league/presenters/LeagueStatsPresenter.ts b/apps/api/src/domain/league/presenters/LeagueStatsPresenter.ts index 8a1311d67..416bd89ad 100644 --- a/apps/api/src/domain/league/presenters/LeagueStatsPresenter.ts +++ b/apps/api/src/domain/league/presenters/LeagueStatsPresenter.ts @@ -1,17 +1,19 @@ -import { ILeagueStatsPresenter, LeagueStatsResultDTO, LeagueStatsViewModel } from '@core/racing/application/presenters/ILeagueStatsPresenter'; +import { LeagueStatsOutputPort } from '@core/racing/application/ports/output/LeagueStatsOutputPort'; +import { LeagueStatsDTO } from '../dtos/LeagueStatsDTO'; +import type { Presenter } from '@core/shared/presentation'; -export class LeagueStatsPresenter implements ILeagueStatsPresenter { - private result: LeagueStatsViewModel | null = null; +export class LeagueStatsPresenter implements Presenter { + private result: LeagueStatsDTO | null = null; reset() { this.result = null; } - present(dto: LeagueStatsResultDTO) { + present(dto: LeagueStatsOutputPort) { this.result = dto; } - getViewModel(): LeagueStatsViewModel | null { + getViewModel(): LeagueStatsDTO | null { return this.result; } } \ No newline at end of file diff --git a/apps/api/src/domain/league/presenters/RejectLeagueJoinRequestPresenter.ts b/apps/api/src/domain/league/presenters/RejectLeagueJoinRequestPresenter.ts index 685fc6c29..c65eeef8e 100644 --- a/apps/api/src/domain/league/presenters/RejectLeagueJoinRequestPresenter.ts +++ b/apps/api/src/domain/league/presenters/RejectLeagueJoinRequestPresenter.ts @@ -1,18 +1,9 @@ -import { IRejectLeagueJoinRequestPresenter, RejectLeagueJoinRequestResultDTO, RejectLeagueJoinRequestViewModel } from '@core/racing/application/presenters/IRejectLeagueJoinRequestPresenter'; +import type { RejectLeagueJoinRequestOutputPort } from '@core/racing/application/ports/output/RejectLeagueJoinRequestOutputPort'; +import type { RejectJoinRequestOutputDTO } from '../dtos/RejectJoinRequestOutputDTO'; -export class RejectLeagueJoinRequestPresenter implements IRejectLeagueJoinRequestPresenter { - private result: RejectLeagueJoinRequestViewModel | null = null; - - reset() { - this.result = null; - } - - present(dto: RejectLeagueJoinRequestResultDTO) { - this.result = dto; - } - - getViewModel(): RejectLeagueJoinRequestViewModel { - if (!this.result) throw new Error('Presenter not presented'); - return this.result; - } +export function mapRejectLeagueJoinRequestOutputPortToDTO(port: RejectLeagueJoinRequestOutputPort): RejectJoinRequestOutputDTO { + return { + success: port.success, + message: port.message, + }; } \ No newline at end of file diff --git a/apps/api/src/domain/league/presenters/RemoveLeagueMemberPresenter.ts b/apps/api/src/domain/league/presenters/RemoveLeagueMemberPresenter.ts index 9e819fc87..242f09fe5 100644 --- a/apps/api/src/domain/league/presenters/RemoveLeagueMemberPresenter.ts +++ b/apps/api/src/domain/league/presenters/RemoveLeagueMemberPresenter.ts @@ -1,18 +1,8 @@ -import { IRemoveLeagueMemberPresenter, RemoveLeagueMemberResultDTO, RemoveLeagueMemberViewModel } from '@core/racing/application/presenters/IRemoveLeagueMemberPresenter'; +import type { RemoveLeagueMemberOutputPort } from '@core/racing/application/ports/output/RemoveLeagueMemberOutputPort'; +import type { RemoveLeagueMemberOutputDTO } from '../dtos/RemoveLeagueMemberOutputDTO'; -export class RemoveLeagueMemberPresenter implements IRemoveLeagueMemberPresenter { - private result: RemoveLeagueMemberViewModel | null = null; - - reset() { - this.result = null; - } - - present(dto: RemoveLeagueMemberResultDTO) { - this.result = dto; - } - - getViewModel(): RemoveLeagueMemberViewModel { - if (!this.result) throw new Error('Presenter not presented'); - return this.result; - } +export function mapRemoveLeagueMemberOutputPortToDTO(port: RemoveLeagueMemberOutputPort): RemoveLeagueMemberOutputDTO { + return { + success: port.success, + }; } \ No newline at end of file diff --git a/apps/api/src/domain/league/presenters/TotalLeaguesPresenter.ts b/apps/api/src/domain/league/presenters/TotalLeaguesPresenter.ts index 3b9d5acc7..573be4856 100644 --- a/apps/api/src/domain/league/presenters/TotalLeaguesPresenter.ts +++ b/apps/api/src/domain/league/presenters/TotalLeaguesPresenter.ts @@ -1,20 +1,20 @@ -import { IGetTotalLeaguesPresenter, GetTotalLeaguesResultDTO, GetTotalLeaguesViewModel } from '@core/racing/application/presenters/IGetTotalLeaguesPresenter'; -import { LeagueStatsDTO } from '../dtos/LeagueStatsDTO'; +import { GetTotalLeaguesOutputPort } from '@core/racing/application/ports/output/GetTotalLeaguesOutputPort'; +import { TotalLeaguesDTO } from '../dtos/TotalLeaguesDTO'; -export class TotalLeaguesPresenter implements IGetTotalLeaguesPresenter { - private result: LeagueStatsDto | null = null; +export class TotalLeaguesPresenter { + private result: TotalLeaguesDTO | null = null; reset() { this.result = null; } - present(dto: GetTotalLeaguesResultDTO) { + present(output: GetTotalLeaguesOutputPort) { this.result = { - totalLeagues: dto.totalLeagues, + totalLeagues: output.totalLeagues, }; } - getViewModel(): LeagueStatsDto | null { + getViewModel(): TotalLeaguesDTO | null { return this.result; } } \ No newline at end of file diff --git a/apps/api/src/domain/league/presenters/TransferLeagueOwnershipPresenter.ts b/apps/api/src/domain/league/presenters/TransferLeagueOwnershipPresenter.ts index 3b1bba5fe..ec3b7d1d1 100644 --- a/apps/api/src/domain/league/presenters/TransferLeagueOwnershipPresenter.ts +++ b/apps/api/src/domain/league/presenters/TransferLeagueOwnershipPresenter.ts @@ -1,20 +1,8 @@ -import { ITransferLeagueOwnershipPresenter, TransferLeagueOwnershipResultDTO, TransferLeagueOwnershipViewModel } from '@core/racing/application/presenters/ITransferLeagueOwnershipPresenter'; +import type { TransferLeagueOwnershipOutputPort } from '@core/racing/application/ports/output/TransferLeagueOwnershipOutputPort'; +import type { TransferLeagueOwnershipOutputDTO } from '../dtos/TransferLeagueOwnershipOutputDTO'; -export class TransferLeagueOwnershipPresenter implements ITransferLeagueOwnershipPresenter { - private result: TransferLeagueOwnershipViewModel | null = null; - - reset() { - this.result = null; - } - - present(dto: TransferLeagueOwnershipResultDTO): void { - this.result = { - success: dto.success, - }; - } - - getViewModel(): TransferLeagueOwnershipViewModel { - if (!this.result) throw new Error('Presenter not presented'); - return this.result; - } +export function mapTransferLeagueOwnershipOutputPortToDTO(port: TransferLeagueOwnershipOutputPort): TransferLeagueOwnershipOutputDTO { + return { + success: port.success, + }; } \ No newline at end of file diff --git a/apps/api/src/domain/league/presenters/UpdateLeagueMemberRolePresenter.ts b/apps/api/src/domain/league/presenters/UpdateLeagueMemberRolePresenter.ts index 6a520d339..5b7065b1a 100644 --- a/apps/api/src/domain/league/presenters/UpdateLeagueMemberRolePresenter.ts +++ b/apps/api/src/domain/league/presenters/UpdateLeagueMemberRolePresenter.ts @@ -1,18 +1,8 @@ -import { IUpdateLeagueMemberRolePresenter, UpdateLeagueMemberRoleResultDTO, UpdateLeagueMemberRoleViewModel } from '@core/racing/application/presenters/IUpdateLeagueMemberRolePresenter'; +import type { UpdateLeagueMemberRoleOutputPort } from '@core/racing/application/ports/output/UpdateLeagueMemberRoleOutputPort'; +import type { UpdateLeagueMemberRoleOutputDTO } from '../dtos/UpdateLeagueMemberRoleOutputDTO'; -export class UpdateLeagueMemberRolePresenter implements IUpdateLeagueMemberRolePresenter { - private result: UpdateLeagueMemberRoleViewModel | null = null; - - reset() { - this.result = null; - } - - present(dto: UpdateLeagueMemberRoleResultDTO) { - this.result = dto; - } - - getViewModel(): UpdateLeagueMemberRoleViewModel { - if (!this.result) throw new Error('Presenter not presented'); - return this.result; - } +export function mapUpdateLeagueMemberRoleOutputPortToDTO(port: UpdateLeagueMemberRoleOutputPort): UpdateLeagueMemberRoleOutputDTO { + return { + success: port.success, + }; } \ No newline at end of file diff --git a/apps/api/src/domain/race/RaceController.ts b/apps/api/src/domain/race/RaceController.ts index 240e4d257..bc7b20bd2 100644 --- a/apps/api/src/domain/race/RaceController.ts +++ b/apps/api/src/domain/race/RaceController.ts @@ -46,8 +46,8 @@ export class RaceController { @Get('all/page-data') @ApiOperation({ summary: 'Get all races page data' }) - @ApiResponse({ status: 200, description: 'All races page data', type: RacesPageDataDTO }) - async getAllRacesPageData(): Promise { + @ApiResponse({ status: 200, description: 'All races page data', type: AllRacesPageDTO }) + async getAllRacesPageData(): Promise { return this.raceService.getAllRacesPageData(); } diff --git a/apps/api/src/domain/race/RaceProviders.ts b/apps/api/src/domain/race/RaceProviders.ts index 40a7658fb..18f08ac4d 100644 --- a/apps/api/src/domain/race/RaceProviders.ts +++ b/apps/api/src/domain/race/RaceProviders.ts @@ -146,8 +146,6 @@ export const RaceProviders: Provider[] = [ raceRegRepo: IRaceRegistrationRepository, resultRepo: IResultRepository, leagueMembershipRepo: ILeagueMembershipRepository, - driverRatingProvider: DriverRatingProvider, - imageService: IImageServicePort, ) => new GetRaceDetailUseCase( raceRepo, leagueRepo, @@ -155,8 +153,6 @@ export const RaceProviders: Provider[] = [ raceRegRepo, resultRepo, leagueMembershipRepo, - driverRatingProvider, - imageService, ), inject: [ RACE_REPOSITORY_TOKEN, @@ -165,8 +161,6 @@ export const RaceProviders: Provider[] = [ RACE_REGISTRATION_REPOSITORY_TOKEN, RESULT_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, - DRIVER_RATING_PROVIDER_TOKEN, - IMAGE_SERVICE_TOKEN, ], }, { diff --git a/apps/api/src/domain/race/RaceService.ts b/apps/api/src/domain/race/RaceService.ts index c7ef88913..19a110ba7 100644 --- a/apps/api/src/domain/race/RaceService.ts +++ b/apps/api/src/domain/race/RaceService.ts @@ -1,12 +1,11 @@ import { Injectable, Inject } from '@nestjs/common'; import type { AllRacesPageViewModel } from '@core/racing/application/presenters/IGetAllRacesPresenter'; -import type { GetTotalRacesViewModel } from '@core/racing/application/presenters/IGetTotalRacesPresenter'; -import type { RaceDetailViewModel } from '@core/racing/application/presenters/IRaceDetailPresenter'; -import type { RacesPageViewModel } from '@core/racing/application/presenters/IRacesPagePresenter'; -import type { RaceResultsDetailViewModel } from '@core/racing/application/presenters/IRaceResultsDetailPresenter'; -import type { RaceWithSOFViewModel } from '@core/racing/application/presenters/IRaceWithSOFPresenter'; -import type { RaceProtestsViewModel } from '@core/racing/application/presenters/IRaceProtestsPresenter'; -import type { RacePenaltiesViewModel } from '@core/racing/application/presenters/IRacePenaltiesPresenter'; +import type { RaceDetailOutputPort } from '@core/racing/application/ports/output/RaceDetailOutputPort'; +import type { RacesPageOutputPort } from '@core/racing/application/ports/output/RacesPageOutputPort'; +import type { RaceResultsDetailOutputPort } from '@core/racing/application/ports/output/RaceResultsDetailOutputPort'; +import type { RaceWithSOFOutputPort } from '@core/racing/application/ports/output/RaceWithSOFOutputPort'; +import type { RaceProtestsOutputPort } from '@core/racing/application/ports/output/RaceProtestsOutputPort'; +import type { RacePenaltiesOutputPort } from '@core/racing/application/ports/output/RacePenaltiesOutputPort'; // DTOs import { GetRaceDetailParamsDTO } from './dtos/GetRaceDetailParamsDTO'; @@ -14,9 +13,18 @@ import { RegisterForRaceParamsDTO } from './dtos/RegisterForRaceParamsDTO'; import { WithdrawFromRaceParamsDTO } from './dtos/WithdrawFromRaceParamsDTO'; import { RaceActionParamsDTO } from './dtos/RaceActionParamsDTO'; import { ImportRaceResultsDTO } from './dtos/ImportRaceResultsDTO'; +import { AllRacesPageDTO } from './dtos/AllRacesPageDTO'; +import { RaceStatsDTO } from './dtos/RaceStatsDTO'; +import { RacePenaltiesDTO } from './dtos/RacePenaltiesDTO'; +import { RaceProtestsDTO } from './dtos/RaceProtestsDTO'; +import { RaceResultsDetailDTO } from './dtos/RaceResultsDetailDTO'; // Core imports import type { Logger } from '@core/shared/application/Logger'; +import { Result } from '@core/shared/application/Result'; +import { DriverRatingProvider } from '@core/racing/application/ports/DriverRatingProvider'; +import { IImageServicePort } from '@core/racing/application/ports/IImageServicePort'; +import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository'; // Use cases import { GetAllRacesUseCase } from '@core/racing/application/use-cases/GetAllRacesUseCase'; @@ -53,7 +61,7 @@ import { RequestProtestDefenseCommandDTO } from './dtos/RequestProtestDefenseCom import { ReviewProtestCommandDTO } from './dtos/ReviewProtestCommandDTO'; // Tokens -import { LOGGER_TOKEN } from './RaceProviders'; +import { LOGGER_TOKEN, DRIVER_RATING_PROVIDER_TOKEN, IMAGE_SERVICE_TOKEN, LEAGUE_REPOSITORY_TOKEN } from './RaceProviders'; @Injectable() export class RaceService { @@ -77,7 +85,10 @@ export class RaceService { private readonly applyPenaltyUseCase: ApplyPenaltyUseCase, private readonly requestProtestDefenseUseCase: RequestProtestDefenseUseCase, private readonly reviewProtestUseCase: ReviewProtestUseCase, + @Inject(LEAGUE_REPOSITORY_TOKEN) private readonly leagueRepository: ILeagueRepository, @Inject(LOGGER_TOKEN) private readonly logger: Logger, + @Inject(DRIVER_RATING_PROVIDER_TOKEN) private readonly driverRatingProvider: DriverRatingProvider, + @Inject(IMAGE_SERVICE_TOKEN) private readonly imageService: IImageServicePort, ) {} async getAllRaces(): Promise { @@ -88,21 +99,29 @@ export class RaceService { return presenter.getViewModel()!; } - async getTotalRaces(): Promise { + async getTotalRaces(): Promise { this.logger.debug('[RaceService] Fetching total races count.'); + const result = await this.getTotalRacesUseCase.execute(); + if (result.isErr()) { + throw new Error(result.unwrapErr().code); + } const presenter = new GetTotalRacesPresenter(); - await this.getTotalRacesUseCase.execute({}, presenter); + presenter.present(result.unwrap()); return presenter.getViewModel()!; } async importRaceResults(input: ImportRaceResultsDTO): Promise { this.logger.debug('Importing race results:', input); + const result = await this.importRaceResultsApiUseCase.execute({ raceId: input.raceId, resultsFileContent: input.resultsFileContent }); + if (result.isErr()) { + throw new Error(result.unwrapErr().code); + } const presenter = new ImportRaceResultsApiPresenter(); - await this.importRaceResultsApiUseCase.execute({ raceId: input.raceId, resultsFileContent: input.resultsFileContent }, presenter); + presenter.present(result.unwrap()); return presenter.getViewModel()!; } - async getRaceDetail(params: GetRaceDetailParamsDTO): Promise { + async getRaceDetail(params: GetRaceDetailParamsDTO): Promise { this.logger.debug('[RaceService] Fetching race detail:', params); const result = await this.getRaceDetailUseCase.execute(params); @@ -111,10 +130,71 @@ export class RaceService { throw new Error('Failed to get race detail'); } - return result.value; + const outputPort = result.value; + + // Map to DTO + const raceDTO = outputPort.race ? { + id: outputPort.race.id, + leagueId: outputPort.race.leagueId, + track: outputPort.race.track, + car: outputPort.race.car, + scheduledAt: outputPort.race.scheduledAt.toISOString(), + sessionType: outputPort.race.sessionType, + status: outputPort.race.status, + strengthOfField: outputPort.race.strengthOfField ?? null, + registeredCount: outputPort.race.registeredCount ?? undefined, + maxParticipants: outputPort.race.maxParticipants ?? undefined, + } : null; + + const leagueDTO = outputPort.league ? { + id: outputPort.league.id.toString(), + name: outputPort.league.name.toString(), + description: outputPort.league.description.toString(), + settings: { + maxDrivers: outputPort.league.settings.maxDrivers ?? undefined, + qualifyingFormat: outputPort.league.settings.qualifyingFormat ?? undefined, + }, + } : null; + + const entryListDTO = await Promise.all(outputPort.drivers.map(async driver => { + const ratingResult = await this.driverRatingProvider.getDriverRating({ driverId: driver.id }); + const avatarResult = await this.imageService.getDriverAvatar({ driverId: driver.id }); + return { + id: driver.id, + name: driver.name.toString(), + country: driver.country.toString(), + avatarUrl: avatarResult.avatarUrl, + rating: ratingResult.rating, + isCurrentUser: driver.id === params.driverId, + }; + })); + + const registrationDTO = { + isUserRegistered: outputPort.isUserRegistered, + canRegister: outputPort.canRegister, + }; + + const userResultDTO = outputPort.userResult ? { + position: outputPort.userResult.position.toNumber(), + startPosition: outputPort.userResult.startPosition.toNumber(), + incidents: outputPort.userResult.incidents.toNumber(), + fastestLap: outputPort.userResult.fastestLap.toNumber(), + positionChange: outputPort.userResult.getPositionChange(), + isPodium: outputPort.userResult.isPodium(), + isClean: outputPort.userResult.isClean(), + ratingChange: this.calculateRatingChange(outputPort.userResult.position.toNumber()), + } : null; + + return { + race: raceDTO, + league: leagueDTO, + entryList: entryListDTO, + registration: registrationDTO, + userResult: userResultDTO, + }; } - async getRacesPageData(): Promise { + async getRacesPageData(): Promise { this.logger.debug('[RaceService] Fetching races page data.'); const result = await this.getRacesPageDataUseCase.execute(); @@ -123,10 +203,33 @@ export class RaceService { throw new Error('Failed to get races page data'); } - return result.value; + const outputPort = result.value; + + // Fetch leagues for league names + const allLeagues = await this.leagueRepository.findAll(); + const leagueMap = new Map(allLeagues.map(l => [l.id, l.name])); + + // Map to DTO + const racesDTO = outputPort.races.map(race => ({ + id: race.id, + track: race.track, + car: race.car, + scheduledAt: race.scheduledAt.toISOString(), + status: race.status, + leagueId: race.leagueId, + leagueName: leagueMap.get(race.leagueId) ?? 'Unknown League', + strengthOfField: race.strengthOfField, + isUpcoming: race.scheduledAt > new Date(), + isLive: race.status === 'running', + isPast: race.scheduledAt < new Date() && race.status === 'completed', + })); + + return { + races: racesDTO, + }; } - async getAllRacesPageData(): Promise { + async getAllRacesPageData(): Promise { this.logger.debug('[RaceService] Fetching all races page data.'); const result = await this.getAllRacesPageDataUseCase.execute(); @@ -135,10 +238,10 @@ export class RaceService { throw new Error('Failed to get all races page data'); } - return result.value; + return result.value as AllRacesPageDTO; } - async getRaceResultsDetail(raceId: string): Promise { + async getRaceResultsDetail(raceId: string): Promise { this.logger.debug('[RaceService] Fetching race results detail:', { raceId }); const result = await this.getRaceResultsDetailUseCase.execute({ raceId }); @@ -147,10 +250,41 @@ export class RaceService { throw new Error('Failed to get race results detail'); } - return result.value; + const outputPort = result.value; + + // Create a map of driverId to driver for easy lookup + const driverMap = new Map(outputPort.drivers.map(driver => [driver.id, driver])); + + const resultsDTO = await Promise.all(outputPort.results.map(async (result) => { + const driver = driverMap.get(result.driverId.toString()); + if (!driver) { + throw new Error(`Driver not found for result: ${result.driverId}`); + } + + const avatarResult = await this.imageService.getDriverAvatar({ driverId: driver.id }); + + return { + driverId: result.driverId.toString(), + driverName: driver.name.toString(), + avatarUrl: avatarResult.avatarUrl, + position: result.position.toNumber(), + startPosition: result.startPosition.toNumber(), + incidents: result.incidents.toNumber(), + fastestLap: result.fastestLap.toNumber(), + positionChange: result.getPositionChange(), + isPodium: result.isPodium(), + isClean: result.isClean(), + }; + })); + + return { + raceId: outputPort.race.id, + track: outputPort.race.track, + results: resultsDTO, + }; } - async getRaceWithSOF(raceId: string): Promise { + async getRaceWithSOF(raceId: string): Promise { this.logger.debug('[RaceService] Fetching race with SOF:', { raceId }); const result = await this.getRaceWithSOFUseCase.execute({ raceId }); @@ -159,10 +293,17 @@ export class RaceService { throw new Error('Failed to get race with SOF'); } - return result.value; + const outputPort = result.value; + + // Map to DTO + return { + id: outputPort.id, + track: outputPort.track, + strengthOfField: outputPort.strengthOfField, + }; } - async getRaceProtests(raceId: string): Promise { + async getRaceProtests(raceId: string): Promise { this.logger.debug('[RaceService] Fetching race protests:', { raceId }); const result = await this.getRaceProtestsUseCase.execute({ raceId }); @@ -171,10 +312,32 @@ export class RaceService { throw new Error('Failed to get race protests'); } - return result.value; + const outputPort = result.value; + + const protestsDTO = outputPort.protests.map(protest => ({ + id: protest.id, + protestingDriverId: protest.protestingDriverId, + accusedDriverId: protest.accusedDriverId, + incident: { + lap: protest.incident.lap, + description: protest.incident.description, + }, + status: protest.status, + filedAt: protest.filedAt.toISOString(), + })); + + const driverMap: Record = {}; + outputPort.drivers.forEach(driver => { + driverMap[driver.id] = driver.name.toString(); + }); + + return { + protests: protestsDTO, + driverMap, + }; } - async getRacePenalties(raceId: string): Promise { + async getRacePenalties(raceId: string): Promise { this.logger.debug('[RaceService] Fetching race penalties:', { raceId }); const result = await this.getRacePenaltiesUseCase.execute({ raceId }); @@ -183,7 +346,28 @@ export class RaceService { throw new Error('Failed to get race penalties'); } - return result.value; + const outputPort = result.value; + + const penaltiesDTO = outputPort.penalties.map(penalty => ({ + id: penalty.id, + driverId: penalty.driverId, + type: penalty.type, + value: penalty.value ?? 0, + reason: penalty.reason, + issuedBy: penalty.issuedBy, + issuedAt: penalty.issuedAt.toISOString(), + notes: penalty.notes, + })); + + const driverMap: Record = {}; + outputPort.drivers.forEach(driver => { + driverMap[driver.id] = driver.name.toString(); + }); + + return { + penalties: penaltiesDTO, + driverMap, + }; } async registerForRace(params: RegisterForRaceParamsDTO): Promise { @@ -277,4 +461,10 @@ export class RaceService { throw new Error('Failed to review protest'); } } + + private calculateRatingChange(position: number): number { + const baseChange = position <= 3 ? 25 : position <= 10 ? 10 : -5; + const positionBonus = Math.max(0, (20 - position) * 2); + return baseChange + positionBonus; + } } diff --git a/apps/api/src/domain/race/dtos/AllRacesPageDTO.ts b/apps/api/src/domain/race/dtos/AllRacesPageDTO.ts index 34db3e585..60f319c08 100644 --- a/apps/api/src/domain/race/dtos/AllRacesPageDTO.ts +++ b/apps/api/src/domain/race/dtos/AllRacesPageDTO.ts @@ -1,10 +1,45 @@ import { ApiProperty } from '@nestjs/swagger'; -import { RaceViewModel } from '@core/racing/application/presenters/IGetAllRacesPresenter'; -export class AllRacesPageDTO { - @ApiProperty({ type: [RaceViewModel] }) - races!: RaceViewModel[]; +export type AllRacesStatus = 'scheduled' | 'running' | 'completed' | 'cancelled' | 'all'; + +export class AllRacesListItemDTO { + @ApiProperty() + id!: string; @ApiProperty() - totalCount!: number; + track!: string; + + @ApiProperty() + car!: string; + + @ApiProperty() + scheduledAt!: string; + + @ApiProperty() + status!: 'scheduled' | 'running' | 'completed' | 'cancelled'; + + @ApiProperty() + leagueId!: string; + + @ApiProperty() + leagueName!: string; + + @ApiProperty({ nullable: true }) + strengthOfField!: number | null; +} + +export class AllRacesFilterOptionsDTO { + @ApiProperty({ type: [{ value: String, label: String }] }) + statuses!: { value: AllRacesStatus; label: string }[]; + + @ApiProperty({ type: [{ id: String, name: String }] }) + leagues!: { id: string; name: string }[]; +} + +export class AllRacesPageDTO { + @ApiProperty({ type: [AllRacesListItemDTO] }) + races!: AllRacesListItemDTO[]; + + @ApiProperty({ type: AllRacesFilterOptionsDTO }) + filters!: AllRacesFilterOptionsDTO; } \ No newline at end of file diff --git a/apps/api/src/domain/race/presenters/GetAllRacesPresenter.ts b/apps/api/src/domain/race/presenters/GetAllRacesPresenter.ts index 3e3843598..5ef65b969 100644 --- a/apps/api/src/domain/race/presenters/GetAllRacesPresenter.ts +++ b/apps/api/src/domain/race/presenters/GetAllRacesPresenter.ts @@ -1,17 +1,39 @@ -import { IGetAllRacesPresenter, GetAllRacesResultDTO, AllRacesPageViewModel } from '@core/racing/application/presenters/IGetAllRacesPresenter'; +import { GetAllRacesOutputPort } from '@core/racing/application/ports/output/GetAllRacesOutputPort'; +import { AllRacesPageDTO } from '../dtos/AllRacesPageDTO'; -export class GetAllRacesPresenter implements IGetAllRacesPresenter { - private result: AllRacesPageViewModel | null = null; +export class GetAllRacesPresenter { + private result: AllRacesPageDTO | null = null; reset() { this.result = null; } - present(dto: GetAllRacesResultDTO) { - this.result = dto; + async present(output: GetAllRacesOutputPort) { + this.result = { + races: output.races.map(race => ({ + id: race.id, + track: race.track, + car: race.car, + scheduledAt: race.scheduledAt, + status: race.status, + leagueId: race.leagueId, + leagueName: race.leagueName, + strengthOfField: race.strengthOfField, + })), + filters: { + statuses: [ + { value: 'all', label: 'All' }, + { value: 'scheduled', label: 'Scheduled' }, + { value: 'running', label: 'Running' }, + { value: 'completed', label: 'Completed' }, + { value: 'cancelled', label: 'Cancelled' }, + ], + leagues: [], // TODO: populate if needed + }, + }; } - getViewModel(): AllRacesPageViewModel | null { + getViewModel(): AllRacesPageDTO | null { return this.result; } } \ No newline at end of file diff --git a/apps/api/src/domain/race/presenters/GetTotalRacesPresenter.ts b/apps/api/src/domain/race/presenters/GetTotalRacesPresenter.ts index ed3bc6b5a..14eefe99e 100644 --- a/apps/api/src/domain/race/presenters/GetTotalRacesPresenter.ts +++ b/apps/api/src/domain/race/presenters/GetTotalRacesPresenter.ts @@ -1,19 +1,20 @@ -import { IGetTotalRacesPresenter, GetTotalRacesResultDTO, GetTotalRacesViewModel } from '@core/racing/application/presenters/IGetTotalRacesPresenter'; +import { GetTotalRacesOutputPort } from '@core/racing/application/ports/output/GetTotalRacesOutputPort'; +import { RaceStatsDTO } from '../dtos/RaceStatsDTO'; -export class GetTotalRacesPresenter implements IGetTotalRacesPresenter { - private result: GetTotalRacesViewModel | null = null; +export class GetTotalRacesPresenter { + private result: RaceStatsDTO | null = null; reset() { this.result = null; } - present(dto: GetTotalRacesResultDTO) { + present(output: GetTotalRacesOutputPort) { this.result = { - totalRaces: dto.totalRaces, + totalRaces: output.totalRaces, }; } - getViewModel(): GetTotalRacesViewModel | null { + getViewModel(): RaceStatsDTO | null { return this.result; } } \ No newline at end of file diff --git a/apps/api/src/domain/race/presenters/ImportRaceResultsApiPresenter.ts b/apps/api/src/domain/race/presenters/ImportRaceResultsApiPresenter.ts index 21f4c43b2..d93fc22d5 100644 --- a/apps/api/src/domain/race/presenters/ImportRaceResultsApiPresenter.ts +++ b/apps/api/src/domain/race/presenters/ImportRaceResultsApiPresenter.ts @@ -1,17 +1,24 @@ -import { IImportRaceResultsApiPresenter, ImportRaceResultsApiResultDTO, ImportRaceResultsSummaryViewModel } from '@core/racing/application/presenters/IImportRaceResultsApiPresenter'; +import { ImportRaceResultsApiOutputPort } from '@core/racing/application/ports/output/ImportRaceResultsApiOutputPort'; +import { ImportRaceResultsSummaryDTO } from '../dtos/ImportRaceResultsSummaryDTO'; -export class ImportRaceResultsApiPresenter implements IImportRaceResultsApiPresenter { - private result: ImportRaceResultsSummaryViewModel | null = null; +export class ImportRaceResultsApiPresenter { + private result: ImportRaceResultsSummaryDTO | null = null; reset() { this.result = null; } - present(dto: ImportRaceResultsApiResultDTO) { - this.result = dto; + present(output: ImportRaceResultsApiOutputPort) { + this.result = { + success: output.success, + raceId: output.raceId, + driversProcessed: output.driversProcessed, + resultsRecorded: output.resultsRecorded, + errors: output.errors, + }; } - getViewModel(): ImportRaceResultsSummaryViewModel | null { + getViewModel(): ImportRaceResultsSummaryDTO | null { return this.result; } } \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/dtos/GetEntitySponsorshipPricingResultDTO.ts b/apps/api/src/domain/sponsor/dtos/GetEntitySponsorshipPricingResultDTO.ts index a23ae91b3..9f2d54474 100644 --- a/apps/api/src/domain/sponsor/dtos/GetEntitySponsorshipPricingResultDTO.ts +++ b/apps/api/src/domain/sponsor/dtos/GetEntitySponsorshipPricingResultDTO.ts @@ -2,6 +2,12 @@ import { ApiProperty } from '@nestjs/swagger'; import { SponsorshipPricingItemDTO } from './SponsorshipPricingItemDTO'; export class GetEntitySponsorshipPricingResultDTO { + @ApiProperty() + entityType: string; + + @ApiProperty() + entityId: string; + @ApiProperty({ type: [SponsorshipPricingItemDTO] }) pricing: SponsorshipPricingItemDTO[]; } \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/dtos/SponsorDTO.ts b/apps/api/src/domain/sponsor/dtos/SponsorDTO.ts index cd40fbe33..2f6589481 100644 --- a/apps/api/src/domain/sponsor/dtos/SponsorDTO.ts +++ b/apps/api/src/domain/sponsor/dtos/SponsorDTO.ts @@ -7,9 +7,15 @@ export class SponsorDTO { @ApiProperty() name: string; + @ApiProperty({ required: false }) + contactEmail?: string; + @ApiProperty({ required: false }) logoUrl?: string; @ApiProperty({ required: false }) websiteUrl?: string; + + @ApiProperty({ required: false }) + createdAt?: Date; } \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/presenters/CreateSponsorPresenter.test.ts b/apps/api/src/domain/sponsor/presenters/CreateSponsorPresenter.test.ts index 1f98ffd5c..20eebf9b2 100644 --- a/apps/api/src/domain/sponsor/presenters/CreateSponsorPresenter.test.ts +++ b/apps/api/src/domain/sponsor/presenters/CreateSponsorPresenter.test.ts @@ -10,9 +10,9 @@ describe('CreateSponsorPresenter', () => { describe('reset', () => { it('should reset the result to null', () => { - const mockResult = { id: 'sponsor-1', name: 'Test Sponsor' }; - presenter.present(mockResult); - expect(presenter.viewModel).toEqual(mockResult); + const mockPort = { sponsor: { id: 'sponsor-1', name: 'Test Sponsor', contactEmail: 'test@example.com', createdAt: new Date() } }; + presenter.present(mockPort); + expect(presenter.viewModel).toEqual({ sponsor: mockPort.sponsor }); presenter.reset(); expect(() => presenter.viewModel).toThrow('Presenter not presented'); @@ -21,11 +21,11 @@ describe('CreateSponsorPresenter', () => { describe('present', () => { it('should store the result', () => { - const mockResult = { id: 'sponsor-1', name: 'Test Sponsor', contactEmail: 'test@example.com' }; + const mockPort = { sponsor: { id: 'sponsor-1', name: 'Test Sponsor', contactEmail: 'test@example.com', createdAt: new Date() } }; - presenter.present(mockResult); + presenter.present(mockPort); - expect(presenter.viewModel).toEqual(mockResult); + expect(presenter.viewModel).toEqual({ sponsor: mockPort.sponsor }); }); }); @@ -35,10 +35,10 @@ describe('CreateSponsorPresenter', () => { }); it('should return the result when presented', () => { - const mockResult = { id: 'sponsor-1', name: 'Test Sponsor' }; - presenter.present(mockResult); + const mockPort = { sponsor: { id: 'sponsor-1', name: 'Test Sponsor', contactEmail: 'test@example.com', createdAt: new Date() } }; + presenter.present(mockPort); - expect(presenter.getViewModel()).toEqual(mockResult); + expect(presenter.getViewModel()).toEqual({ sponsor: mockPort.sponsor }); }); }); @@ -48,10 +48,10 @@ describe('CreateSponsorPresenter', () => { }); it('should return the result when presented', () => { - const mockResult = { id: 'sponsor-1', name: 'Test Sponsor' }; - presenter.present(mockResult); + const mockPort = { sponsor: { id: 'sponsor-1', name: 'Test Sponsor', contactEmail: 'test@example.com', createdAt: new Date() } }; + presenter.present(mockPort); - expect(presenter.viewModel).toEqual(mockResult); + expect(presenter.viewModel).toEqual({ sponsor: mockPort.sponsor }); }); }); }); \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/presenters/CreateSponsorPresenter.ts b/apps/api/src/domain/sponsor/presenters/CreateSponsorPresenter.ts index 8ec2aad69..a4cf04623 100644 --- a/apps/api/src/domain/sponsor/presenters/CreateSponsorPresenter.ts +++ b/apps/api/src/domain/sponsor/presenters/CreateSponsorPresenter.ts @@ -1,21 +1,31 @@ -import { CreateSponsorViewModel, CreateSponsorOutputPort, ICreateSponsorPresenter } from '@core/racing/application/presenters/ICreateSponsorPresenter'; +import type { CreateSponsorOutputPort } from '@core/racing/application/ports/output/CreateSponsorOutputPort'; +import type { CreateSponsorOutputDTO } from '../dtos/CreateSponsorOutputDTO'; -export class CreateSponsorPresenter implements ICreateSponsorPresenter { - private result: CreateSponsorViewModel | null = null; +export class CreateSponsorPresenter { + private result: CreateSponsorOutputDTO | null = null; reset() { this.result = null; } - present(dto: CreateSponsorOutputPort) { - this.result = dto; + present(port: CreateSponsorOutputPort) { + this.result = { + sponsor: { + id: port.sponsor.id, + name: port.sponsor.name, + contactEmail: port.sponsor.contactEmail, + logoUrl: port.sponsor.logoUrl, + websiteUrl: port.sponsor.websiteUrl, + createdAt: port.sponsor.createdAt, + }, + }; } - getViewModel(): CreateSponsorViewModel | null { + getViewModel(): CreateSponsorOutputDTO | null { return this.result; } - get viewModel(): CreateSponsorViewModel { + get viewModel(): CreateSponsorOutputDTO { if (!this.result) throw new Error('Presenter not presented'); return this.result; } diff --git a/apps/api/src/domain/sponsor/presenters/GetEntitySponsorshipPricingPresenter.ts b/apps/api/src/domain/sponsor/presenters/GetEntitySponsorshipPricingPresenter.ts index d8935645f..b8b28e434 100644 --- a/apps/api/src/domain/sponsor/presenters/GetEntitySponsorshipPricingPresenter.ts +++ b/apps/api/src/domain/sponsor/presenters/GetEntitySponsorshipPricingPresenter.ts @@ -1,22 +1,41 @@ -import type { GetEntitySponsorshipPricingResultDTO } from '@core/racing/application/use-cases/GetEntitySponsorshipPricingUseCase'; -import type { IEntitySponsorshipPricingPresenter } from '@core/racing/application/presenters/IEntitySponsorshipPricingPresenter'; +import type { GetEntitySponsorshipPricingOutputPort } from '@core/racing/application/ports/output/GetEntitySponsorshipPricingOutputPort'; +import { GetEntitySponsorshipPricingResultDTO } from '../dtos/GetEntitySponsorshipPricingResultDTO'; -export class GetEntitySponsorshipPricingPresenter implements IEntitySponsorshipPricingPresenter { +export class GetEntitySponsorshipPricingPresenter { private result: GetEntitySponsorshipPricingResultDTO | null = null; reset() { this.result = null; } - present(dto: GetEntitySponsorshipPricingResultDTO | null) { - this.result = dto; + async present(output: GetEntitySponsorshipPricingOutputPort | null) { + if (!output) { + this.result = { pricing: [] }; + return; + } + + const pricing = []; + if (output.mainSlot) { + pricing.push({ + id: `${output.entityType}-${output.entityId}-main`, + level: 'main', + price: output.mainSlot.price, + currency: output.mainSlot.currency, + }); + } + if (output.secondarySlot) { + pricing.push({ + id: `${output.entityType}-${output.entityId}-secondary`, + level: 'secondary', + price: output.secondarySlot.price, + currency: output.secondarySlot.currency, + }); + } + + this.result = { pricing }; } getViewModel(): GetEntitySponsorshipPricingResultDTO | null { return this.result; } - - get viewModel(): GetEntitySponsorshipPricingResultDTO | null { - return this.result; - } } \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/presenters/GetPendingSponsorshipRequestsPresenter.ts b/apps/api/src/domain/sponsor/presenters/GetPendingSponsorshipRequestsPresenter.ts new file mode 100644 index 000000000..169a2b774 --- /dev/null +++ b/apps/api/src/domain/sponsor/presenters/GetPendingSponsorshipRequestsPresenter.ts @@ -0,0 +1,13 @@ +import type { PendingSponsorshipRequestsOutputPort } from '@core/racing/application/ports/output/PendingSponsorshipRequestsOutputPort'; +import { GetPendingSponsorshipRequestsOutputDTO } from '../dtos/GetPendingSponsorshipRequestsOutputDTO'; + +export class GetPendingSponsorshipRequestsPresenter { + present(outputPort: PendingSponsorshipRequestsOutputPort): GetPendingSponsorshipRequestsOutputDTO { + return { + entityType: outputPort.entityType, + entityId: outputPort.entityId, + requests: outputPort.requests, + totalCount: outputPort.totalCount, + }; + } +} \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/presenters/GetSponsorDashboardPresenter.ts b/apps/api/src/domain/sponsor/presenters/GetSponsorDashboardPresenter.ts index 57dfa52c0..6619fcf7f 100644 --- a/apps/api/src/domain/sponsor/presenters/GetSponsorDashboardPresenter.ts +++ b/apps/api/src/domain/sponsor/presenters/GetSponsorDashboardPresenter.ts @@ -1,22 +1,8 @@ -import type { SponsorDashboardDTO } from '@core/racing/application/use-cases/GetSponsorDashboardUseCase'; -import type { ISponsorDashboardPresenter, SponsorDashboardViewModel } from '@core/racing/application/presenters/ISponsorDashboardPresenter'; +import type { SponsorDashboardOutputPort } from '@core/racing/application/ports/output/SponsorDashboardOutputPort'; +import { SponsorDashboardDTO } from '../dtos/SponsorDashboardDTO'; -export class GetSponsorDashboardPresenter implements ISponsorDashboardPresenter { - private result: SponsorDashboardViewModel | null = null; - - reset() { - this.result = null; - } - - present(dto: SponsorDashboardDTO | null) { - this.result = dto; - } - - getViewModel(): SponsorDashboardViewModel | null { - return this.result; - } - - get viewModel(): SponsorDashboardViewModel | null { - return this.result; +export class GetSponsorDashboardPresenter { + present(outputPort: SponsorDashboardOutputPort | null): SponsorDashboardDTO | null { + return outputPort; } } \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/presenters/GetSponsorSponsorshipsPresenter.ts b/apps/api/src/domain/sponsor/presenters/GetSponsorSponsorshipsPresenter.ts index 28c1c0b62..84b7a0dc6 100644 --- a/apps/api/src/domain/sponsor/presenters/GetSponsorSponsorshipsPresenter.ts +++ b/apps/api/src/domain/sponsor/presenters/GetSponsorSponsorshipsPresenter.ts @@ -1,22 +1,8 @@ -import type { SponsorSponsorshipsDTO } from '@core/racing/application/use-cases/GetSponsorSponsorshipsUseCase'; -import type { ISponsorSponsorshipsPresenter, SponsorSponsorshipsViewModel } from '@core/racing/application/presenters/ISponsorSponsorshipsPresenter'; +import type { SponsorSponsorshipsOutputPort } from '@core/racing/application/ports/output/SponsorSponsorshipsOutputPort'; +import { SponsorSponsorshipsDTO } from '../dtos/SponsorSponsorshipsDTO'; -export class GetSponsorSponsorshipsPresenter implements ISponsorSponsorshipsPresenter { - private result: SponsorSponsorshipsViewModel | null = null; - - reset() { - this.result = null; - } - - present(dto: SponsorSponsorshipsDTO | null) { - this.result = dto; - } - - getViewModel(): SponsorSponsorshipsViewModel | null { - return this.result; - } - - get viewModel(): SponsorSponsorshipsViewModel | null { - return this.result; +export class GetSponsorSponsorshipsPresenter { + present(outputPort: SponsorSponsorshipsOutputPort | null): SponsorSponsorshipsDTO | null { + return outputPort; } } \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/presenters/GetSponsorsPresenter.ts b/apps/api/src/domain/sponsor/presenters/GetSponsorsPresenter.ts index 31e719a03..cb2d98098 100644 --- a/apps/api/src/domain/sponsor/presenters/GetSponsorsPresenter.ts +++ b/apps/api/src/domain/sponsor/presenters/GetSponsorsPresenter.ts @@ -1,22 +1,10 @@ -import { GetSponsorsViewModel, GetSponsorsResultDTO, IGetSponsorsPresenter } from '@core/racing/application/presenters/IGetSponsorsPresenter'; +import type { GetSponsorsOutputPort } from '@core/racing/application/ports/output/GetSponsorsOutputPort'; +import { GetSponsorsOutputDTO } from '../dtos/GetSponsorsOutputDTO'; -export class GetSponsorsPresenter implements IGetSponsorsPresenter { - private result: GetSponsorsViewModel | null = null; - - reset() { - this.result = null; - } - - present(dto: GetSponsorsResultDTO) { - this.result = dto; - } - - getViewModel(): GetSponsorsViewModel | null { - return this.result; - } - - get viewModel(): GetSponsorsViewModel { - if (!this.result) throw new Error('Presenter not presented'); - return this.result; +export class GetSponsorsPresenter { + present(outputPort: GetSponsorsOutputPort): GetSponsorsOutputDTO { + return { + sponsors: outputPort.sponsors, + }; } } \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/presenters/GetSponsorshipPricingPresenter.ts b/apps/api/src/domain/sponsor/presenters/GetSponsorshipPricingPresenter.ts index 169758521..029fbea6d 100644 --- a/apps/api/src/domain/sponsor/presenters/GetSponsorshipPricingPresenter.ts +++ b/apps/api/src/domain/sponsor/presenters/GetSponsorshipPricingPresenter.ts @@ -1,22 +1,12 @@ -import { GetSponsorshipPricingViewModel, GetSponsorshipPricingResultDTO, IGetSponsorshipPricingPresenter } from '@core/racing/application/presenters/IGetSponsorshipPricingPresenter'; +import type { GetSponsorshipPricingOutputPort } from '@core/racing/application/ports/output/GetSponsorshipPricingOutputPort'; +import { GetEntitySponsorshipPricingResultDTO } from '../dtos/GetEntitySponsorshipPricingResultDTO'; -export class GetSponsorshipPricingPresenter implements IGetSponsorshipPricingPresenter { - private result: GetSponsorshipPricingViewModel | null = null; - - reset() { - this.result = null; - } - - present(dto: GetSponsorshipPricingResultDTO) { - this.result = dto; - } - - getViewModel(): GetSponsorshipPricingViewModel | null { - return this.result; - } - - get viewModel(): GetSponsorshipPricingViewModel { - if (!this.result) throw new Error('Presenter not presented'); - return this.result; +export class GetSponsorshipPricingPresenter { + present(outputPort: GetSponsorshipPricingOutputPort): GetEntitySponsorshipPricingResultDTO { + return { + entityType: outputPort.entityType, + entityId: outputPort.entityId, + pricing: outputPort.pricing, + }; } } \ No newline at end of file diff --git a/apps/api/src/domain/team/dtos/GetTeamsLeaderboardOutputDTO.ts b/apps/api/src/domain/team/dtos/GetTeamsLeaderboardOutputDTO.ts new file mode 100644 index 000000000..2d055e538 --- /dev/null +++ b/apps/api/src/domain/team/dtos/GetTeamsLeaderboardOutputDTO.ts @@ -0,0 +1,58 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export type SkillLevel = 'beginner' | 'intermediate' | 'advanced' | 'pro'; + +class TeamLeaderboardItemDTO { + @ApiProperty() + id: string; + + @ApiProperty() + name: string; + + @ApiProperty() + memberCount: number; + + @ApiProperty({ nullable: true }) + rating: number | null; + + @ApiProperty() + totalWins: number; + + @ApiProperty() + totalRaces: number; + + @ApiProperty({ enum: ['beginner', 'intermediate', 'advanced', 'pro'] }) + performanceLevel: SkillLevel; + + @ApiProperty() + isRecruiting: boolean; + + @ApiProperty() + createdAt: string; + + @ApiProperty({ required: false }) + description?: string; + + @ApiProperty({ enum: ['endurance', 'sprint', 'mixed'], required: false }) + specialization?: 'endurance' | 'sprint' | 'mixed'; + + @ApiProperty({ required: false }) + region?: string; + + @ApiProperty({ type: [String], required: false }) + languages?: string[]; +} + +export class GetTeamsLeaderboardOutputDTO { + @ApiProperty({ type: [TeamLeaderboardItemDTO] }) + teams: TeamLeaderboardItemDTO[]; + + @ApiProperty() + recruitingCount: number; + + @ApiProperty({ type: 'object', additionalProperties: { type: 'array', items: { $ref: '#/components/schemas/TeamLeaderboardItemDTO' } } }) + groupsBySkillLevel: Record; + + @ApiProperty({ type: [TeamLeaderboardItemDTO] }) + topTeams: TeamLeaderboardItemDTO[]; +} \ No newline at end of file diff --git a/apps/api/src/domain/team/presenters/AllTeamsPresenter.ts b/apps/api/src/domain/team/presenters/AllTeamsPresenter.ts index 16e81781c..0fd910e77 100644 --- a/apps/api/src/domain/team/presenters/AllTeamsPresenter.ts +++ b/apps/api/src/domain/team/presenters/AllTeamsPresenter.ts @@ -1,33 +1,33 @@ -import { IAllTeamsPresenter, AllTeamsResultDTO, AllTeamsViewModel, TeamListItemViewModel } from '@core/racing/application/presenters/IAllTeamsPresenter'; +import { GetAllTeamsOutputPort } from '@core/racing/application/ports/output/GetAllTeamsOutputPort'; +import { GetAllTeamsOutputDTO } from '../dtos/GetAllTeamsOutputDTO'; -export class AllTeamsPresenter implements IAllTeamsPresenter { - private result: AllTeamsViewModel | null = null; +export class AllTeamsPresenter { + private result: GetAllTeamsOutputDTO | null = null; reset() { this.result = null; } - present(dto: AllTeamsResultDTO) { - const teams: TeamListItemViewModel[] = dto.teams.map(team => ({ - id: team.id, - name: team.name, - tag: team.tag, - description: team.description, - memberCount: team.memberCount, - leagues: team.leagues || [], - })); - + async present(output: GetAllTeamsOutputPort) { this.result = { - teams, - totalCount: teams.length, + teams: output.teams.map(team => ({ + id: team.id, + name: team.name, + tag: team.tag, + description: team.description, + memberCount: team.memberCount, + leagues: team.leagues || [], + // Note: specialization, region, languages not available in output port + })), + totalCount: output.totalCount || output.teams.length, }; } - getViewModel(): AllTeamsViewModel | null { + getViewModel(): GetAllTeamsOutputDTO | null { return this.result; } - get viewModel(): AllTeamsViewModel { + get viewModel(): GetAllTeamsOutputDTO { if (!this.result) throw new Error('Presenter not presented'); return this.result; } diff --git a/apps/api/src/domain/team/presenters/DriverTeamPresenter.ts b/apps/api/src/domain/team/presenters/DriverTeamPresenter.ts index 90b5f6e2d..e44e0f4b4 100644 --- a/apps/api/src/domain/team/presenters/DriverTeamPresenter.ts +++ b/apps/api/src/domain/team/presenters/DriverTeamPresenter.ts @@ -1,41 +1,38 @@ -import { IDriverTeamPresenter, DriverTeamResultDTO, DriverTeamViewModel } from '@core/racing/application/presenters/IDriverTeamPresenter'; +import { DriverTeamOutputPort } from '@core/racing/application/ports/output/DriverTeamOutputPort'; +import { GetDriverTeamOutputDTO } from '../dtos/GetDriverTeamOutputDTO'; -export class DriverTeamPresenter implements IDriverTeamPresenter { - private result: DriverTeamViewModel | null = null; +export class DriverTeamPresenter { + private result: GetDriverTeamOutputDTO | null = null; reset() { this.result = null; } - present(dto: DriverTeamResultDTO) { - const isOwner = dto.team.ownerId === dto.driverId; - const canManage = isOwner || dto.membership.role === 'owner' || dto.membership.role === 'manager'; + async present(output: DriverTeamOutputPort) { + const isOwner = output.team.ownerId === output.driverId; + const canManage = isOwner || output.membership.role === 'owner' || output.membership.role === 'manager'; this.result = { team: { - id: dto.team.id, - name: dto.team.name, - tag: dto.team.tag, - description: dto.team.description || '', - ownerId: dto.team.ownerId, - leagues: dto.team.leagues || [], + id: output.team.id, + name: output.team.name, + tag: output.team.tag, + description: output.team.description || '', + ownerId: output.team.ownerId, + leagues: output.team.leagues || [], + createdAt: output.team.createdAt.toISOString(), }, membership: { - role: dto.membership.role as 'owner' | 'manager' | 'member', - joinedAt: dto.membership.joinedAt.toISOString(), - isActive: dto.membership.status === 'active', + role: output.membership.role === 'driver' ? 'member' : output.membership.role, + joinedAt: output.membership.joinedAt.toISOString(), + isActive: output.membership.status === 'active', }, isOwner, canManage, }; } - getViewModel(): DriverTeamViewModel | null { - return this.result; - } - - get viewModel(): DriverTeamViewModel { - if (!this.result) throw new Error('Presenter not presented'); + getViewModel(): GetDriverTeamOutputDTO | null { return this.result; } } \ No newline at end of file diff --git a/apps/api/src/domain/team/presenters/TeamDetailsPresenter.ts b/apps/api/src/domain/team/presenters/TeamDetailsPresenter.ts index cc083bdde..0b9a9c30d 100644 --- a/apps/api/src/domain/team/presenters/TeamDetailsPresenter.ts +++ b/apps/api/src/domain/team/presenters/TeamDetailsPresenter.ts @@ -1,50 +1,36 @@ -import { - ITeamDetailsPresenter, - TeamDetailsResultDTO, - TeamDetailsViewModel, -} from '@core/racing/application/presenters/ITeamDetailsPresenter'; +import type { GetTeamDetailsOutputPort } from '@core/racing/application/ports/output/GetTeamDetailsOutputPort'; +import type { GetTeamDetailsOutputDTO } from '../dtos/GetTeamDetailsOutputDTO'; -export class TeamDetailsPresenter implements ITeamDetailsPresenter { - private result: TeamDetailsViewModel | null = null; +export class TeamDetailsPresenter { + private result: GetTeamDetailsOutputDTO | null = null; reset() { this.result = null; } - present(dto: TeamDetailsResultDTO) { - const { team, membership } = dto; - - const canManage = - membership !== null && - (membership.role === 'owner' || membership.role === 'manager'); - + async present(outputPort: GetTeamDetailsOutputPort): Promise { this.result = { team: { - id: team.id, - name: team.name, - tag: team.tag, - description: team.description, - ownerId: team.ownerId, - leagues: team.leagues || [], - createdAt: team.createdAt?.toISOString() || new Date().toISOString(), + id: outputPort.team.id, + name: outputPort.team.name, + tag: outputPort.team.tag, + description: outputPort.team.description, + ownerId: outputPort.team.ownerId, + leagues: outputPort.team.leagues, + createdAt: outputPort.team.createdAt.toISOString(), }, - membership: membership + membership: outputPort.membership ? { - role: membership.role as 'owner' | 'manager' | 'member', - joinedAt: membership.joinedAt.toISOString(), - isActive: membership.status === 'active', + role: outputPort.membership.role, + joinedAt: outputPort.membership.joinedAt.toISOString(), + isActive: outputPort.membership.isActive, } : null, - canManage, + canManage: outputPort.canManage, }; } - getViewModel(): TeamDetailsViewModel | null { - return this.result; - } - - get viewModel(): TeamDetailsViewModel { - if (!this.result) throw new Error('Presenter not presented'); + getViewModel(): GetTeamDetailsOutputDTO | null { return this.result; } } \ No newline at end of file diff --git a/apps/api/src/domain/team/presenters/TeamJoinRequestsPresenter.ts b/apps/api/src/domain/team/presenters/TeamJoinRequestsPresenter.ts index 9d3ca3d46..cd6096bcb 100644 --- a/apps/api/src/domain/team/presenters/TeamJoinRequestsPresenter.ts +++ b/apps/api/src/domain/team/presenters/TeamJoinRequestsPresenter.ts @@ -1,43 +1,30 @@ -import { - ITeamJoinRequestsPresenter, - TeamJoinRequestsResultDTO, - TeamJoinRequestsViewModel, - TeamJoinRequestViewModel, -} from '@core/racing/application/presenters/ITeamJoinRequestsPresenter'; +import type { TeamJoinRequestsOutputPort } from '@core/racing/application/ports/output/TeamJoinRequestsOutputPort'; +import type { GetTeamJoinRequestsOutputDTO } from '../dtos/GetTeamJoinRequestsOutputDTO'; -export class TeamJoinRequestsPresenter implements ITeamJoinRequestsPresenter { - private result: TeamJoinRequestsViewModel | null = null; +export class TeamJoinRequestsPresenter { + private result: GetTeamJoinRequestsOutputDTO | null = null; reset() { this.result = null; } - present(dto: TeamJoinRequestsResultDTO) { - const { requests, driverNames, avatarUrls } = dto; - - const requestViewModels: TeamJoinRequestViewModel[] = requests.map((request) => ({ - requestId: request.id, - driverId: request.driverId, - driverName: driverNames[request.driverId] || 'Unknown', - teamId: request.teamId, - status: 'pending' as const, - requestedAt: request.requestedAt.toISOString(), - avatarUrl: avatarUrls[request.driverId] || '', - })); - + async present(outputPort: TeamJoinRequestsOutputPort): Promise { this.result = { - requests: requestViewModels, - pendingCount: requestViewModels.length, - totalCount: requestViewModels.length, + requests: outputPort.requests.map(request => ({ + requestId: request.requestId, + driverId: request.driverId, + driverName: request.driverName, + teamId: request.teamId, + status: request.status, + requestedAt: request.requestedAt.toISOString(), + avatarUrl: request.avatarUrl, + })), + pendingCount: outputPort.pendingCount, + totalCount: outputPort.totalCount, }; } - getViewModel(): TeamJoinRequestsViewModel | null { - return this.result; - } - - get viewModel(): TeamJoinRequestsViewModel { - if (!this.result) throw new Error('Presenter not presented'); + getViewModel(): GetTeamJoinRequestsOutputDTO | null { return this.result; } } \ No newline at end of file diff --git a/apps/api/src/domain/team/presenters/TeamMembersPresenter.ts b/apps/api/src/domain/team/presenters/TeamMembersPresenter.ts index 6c1a4f8c2..b87d11fb3 100644 --- a/apps/api/src/domain/team/presenters/TeamMembersPresenter.ts +++ b/apps/api/src/domain/team/presenters/TeamMembersPresenter.ts @@ -1,48 +1,31 @@ -import { - ITeamMembersPresenter, - TeamMembersResultDTO, - TeamMembersViewModel, - TeamMemberViewModel, -} from '@core/racing/application/presenters/ITeamMembersPresenter'; +import type { TeamMembersOutputPort } from '@core/racing/application/ports/output/TeamMembersOutputPort'; +import type { GetTeamMembersOutputDTO } from '../dtos/GetTeamMembersOutputDTO'; -export class TeamMembersPresenter implements ITeamMembersPresenter { - private result: TeamMembersViewModel | null = null; +export class TeamMembersPresenter { + private result: GetTeamMembersOutputDTO | null = null; reset() { this.result = null; } - present(dto: TeamMembersResultDTO) { - const { memberships, driverNames, avatarUrls } = dto; - - const members: TeamMemberViewModel[] = memberships.map((membership) => ({ - driverId: membership.driverId, - driverName: driverNames[membership.driverId] || 'Unknown', - role: membership.role as 'owner' | 'manager' | 'member', - joinedAt: membership.joinedAt.toISOString(), - isActive: membership.status === 'active', - avatarUrl: avatarUrls[membership.driverId] || '', - })); - - const ownerCount = members.filter((m) => m.role === 'owner').length; - const managerCount = members.filter((m) => m.role === 'manager').length; - const memberCount = members.filter((m) => m.role === 'member').length; - + async present(outputPort: TeamMembersOutputPort): Promise { this.result = { - members, - totalCount: members.length, - ownerCount, - managerCount, - memberCount, + members: outputPort.members.map(member => ({ + driverId: member.driverId, + driverName: member.driverName, + role: member.role, + joinedAt: member.joinedAt.toISOString(), + isActive: member.isActive, + avatarUrl: member.avatarUrl, + })), + totalCount: outputPort.totalCount, + ownerCount: outputPort.ownerCount, + managerCount: outputPort.managerCount, + memberCount: outputPort.memberCount, }; } - getViewModel(): TeamMembersViewModel | null { - return this.result; - } - - get viewModel(): TeamMembersViewModel { - if (!this.result) throw new Error('Presenter not presented'); + getViewModel(): GetTeamMembersOutputDTO | null { return this.result; } } \ No newline at end of file diff --git a/apps/api/src/domain/team/presenters/TeamsLeaderboardPresenter.ts b/apps/api/src/domain/team/presenters/TeamsLeaderboardPresenter.ts index ffe6b3cd1..8885b4a7c 100644 --- a/apps/api/src/domain/team/presenters/TeamsLeaderboardPresenter.ts +++ b/apps/api/src/domain/team/presenters/TeamsLeaderboardPresenter.ts @@ -1,32 +1,112 @@ -import { ITeamsLeaderboardPresenter, TeamsLeaderboardResultDTO, TeamsLeaderboardViewModel, TeamLeaderboardItemViewModel } from '@core/racing/application/presenters/ITeamsLeaderboardPresenter'; +import type { TeamsLeaderboardOutputPort } from '@core/racing/application/ports/output/TeamsLeaderboardOutputPort'; +import type { GetTeamsLeaderboardOutputDTO } from '../dtos/GetTeamsLeaderboardOutputDTO'; -export class TeamsLeaderboardPresenter implements ITeamsLeaderboardPresenter { - private result: TeamsLeaderboardViewModel | null = null; +export class TeamsLeaderboardPresenter { + private result: GetTeamsLeaderboardOutputDTO | null = null; reset() { this.result = null; } - present(dto: TeamsLeaderboardResultDTO) { + async present(outputPort: TeamsLeaderboardOutputPort): Promise { this.result = { - teams: dto.teams as TeamLeaderboardItemViewModel[], - recruitingCount: dto.recruitingCount, + teams: outputPort.teams.map(team => ({ + id: team.id, + name: team.name, + memberCount: team.memberCount, + rating: team.rating, + totalWins: team.totalWins, + totalRaces: team.totalRaces, + performanceLevel: team.performanceLevel, + isRecruiting: team.isRecruiting, + createdAt: team.createdAt.toISOString(), + description: team.description, + specialization: team.specialization, + region: team.region, + languages: team.languages, + })), + recruitingCount: outputPort.recruitingCount, groupsBySkillLevel: { - beginner: [], - intermediate: [], - advanced: [], - pro: [], + beginner: outputPort.groupsBySkillLevel.beginner.map(team => ({ + id: team.id, + name: team.name, + memberCount: team.memberCount, + rating: team.rating, + totalWins: team.totalWins, + totalRaces: team.totalRaces, + performanceLevel: team.performanceLevel, + isRecruiting: team.isRecruiting, + createdAt: team.createdAt.toISOString(), + description: team.description, + specialization: team.specialization, + region: team.region, + languages: team.languages, + })), + intermediate: outputPort.groupsBySkillLevel.intermediate.map(team => ({ + id: team.id, + name: team.name, + memberCount: team.memberCount, + rating: team.rating, + totalWins: team.totalWins, + totalRaces: team.totalRaces, + performanceLevel: team.performanceLevel, + isRecruiting: team.isRecruiting, + createdAt: team.createdAt.toISOString(), + description: team.description, + specialization: team.specialization, + region: team.region, + languages: team.languages, + })), + advanced: outputPort.groupsBySkillLevel.advanced.map(team => ({ + id: team.id, + name: team.name, + memberCount: team.memberCount, + rating: team.rating, + totalWins: team.totalWins, + totalRaces: team.totalRaces, + performanceLevel: team.performanceLevel, + isRecruiting: team.isRecruiting, + createdAt: team.createdAt.toISOString(), + description: team.description, + specialization: team.specialization, + region: team.region, + languages: team.languages, + })), + pro: outputPort.groupsBySkillLevel.pro.map(team => ({ + id: team.id, + name: team.name, + memberCount: team.memberCount, + rating: team.rating, + totalWins: team.totalWins, + totalRaces: team.totalRaces, + performanceLevel: team.performanceLevel, + isRecruiting: team.isRecruiting, + createdAt: team.createdAt.toISOString(), + description: team.description, + specialization: team.specialization, + region: team.region, + languages: team.languages, + })), }, - topTeams: (dto.teams as TeamLeaderboardItemViewModel[]).slice(0, 10), + topTeams: outputPort.topTeams.map(team => ({ + id: team.id, + name: team.name, + memberCount: team.memberCount, + rating: team.rating, + totalWins: team.totalWins, + totalRaces: team.totalRaces, + performanceLevel: team.performanceLevel, + isRecruiting: team.isRecruiting, + createdAt: team.createdAt.toISOString(), + description: team.description, + specialization: team.specialization, + region: team.region, + languages: team.languages, + })), }; } - getViewModel(): TeamsLeaderboardViewModel | null { - return this.result; - } - - get viewModel(): TeamsLeaderboardViewModel { - if (!this.result) throw new Error('Presenter not presented'); + getViewModel(): GetTeamsLeaderboardOutputDTO | null { return this.result; } } \ No newline at end of file diff --git a/apps/website/components/drivers/DriverProfile.tsx b/apps/website/components/drivers/DriverProfile.tsx index 4bc79f553..66b30ac97 100644 --- a/apps/website/components/drivers/DriverProfile.tsx +++ b/apps/website/components/drivers/DriverProfile.tsx @@ -10,7 +10,7 @@ import PerformanceMetrics from './PerformanceMetrics'; import { useEffect, useState } from 'react'; import { DriverTeamPresenter } from '@/lib/presenters/DriverTeamPresenter'; import { getPrimaryLeagueIdForDriver } from '@/lib/leagueMembership'; -import type { ProfileOverviewViewModel } from '@core/racing/application/presenters/IProfileOverviewPresenter'; +import type { ProfileOverviewOutputPort } from '@core/racing/application/ports/output/ProfileOverviewOutputPort'; import type { DriverTeamViewModel } from '@core/racing/application/presenters/IDriverTeamPresenter'; interface DriverProfileProps { @@ -33,7 +33,7 @@ interface DriverProfileStatsViewModel { overallRank?: number; } -type DriverProfileOverviewViewModel = ProfileOverviewViewModel | null; +type DriverProfileOverviewViewModel = ProfileOverviewOutputPort | null; export default function DriverProfile({ driver, isOwnProfile = false, onEditClick }: DriverProfileProps) { const [profileData, setProfileData] = useState(null); @@ -61,8 +61,8 @@ export default function DriverProfile({ driver, isOwnProfile = false, onEditClic const leagueRank = primaryLeagueId ? getLeagueRankings(driver.id, primaryLeagueId) : { rank: 0, totalDrivers: 0, percentile: 0 }; - const globalRank = profileData?.currentDriver?.globalRank ?? null; - const totalDrivers = profileData?.currentDriver?.totalDrivers ?? 0; + const globalRank = profileData?.driver?.globalRank ?? null; + const totalDrivers = profileData?.driver?.totalDrivers ?? 0; const performanceStats = driverStats ? { winRate: driverStats.totalRaces > 0 ? (driverStats.wins / driverStats.totalRaces) * 100 : 0, diff --git a/apps/website/components/drivers/ProfileStats.tsx b/apps/website/components/drivers/ProfileStats.tsx index 75608e789..31213d3b1 100644 --- a/apps/website/components/drivers/ProfileStats.tsx +++ b/apps/website/components/drivers/ProfileStats.tsx @@ -4,7 +4,7 @@ import Card from '../ui/Card'; import RankBadge from './RankBadge'; import { useState, useEffect } from 'react'; import { getPrimaryLeagueIdForDriver } from '@/lib/leagueMembership'; -import type { ProfileOverviewViewModel } from '@core/racing/application/presenters/IProfileOverviewPresenter'; +import type { ProfileOverviewOutputPort } from '@core/racing/application/ports/output/ProfileOverviewOutputPort'; interface ProfileStatsProps { driverId?: string; @@ -18,7 +18,7 @@ interface ProfileStatsProps { }; } -type DriverProfileOverviewViewModel = ProfileOverviewViewModel | null; +type DriverProfileOverviewViewModel = ProfileOverviewOutputPort | null; export default function ProfileStats({ driverId, stats }: ProfileStatsProps) { const [profileData, setProfileData] = useState(null); @@ -35,7 +35,7 @@ export default function ProfileStats({ driverId, stats }: ProfileStatsProps) { }, [driverId]); const driverStats = profileData?.stats || null; - const totalDrivers = profileData?.currentDriver?.totalDrivers ?? 0; + const totalDrivers = profileData?.driver?.totalDrivers ?? 0; const primaryLeagueId = driverId ? getPrimaryLeagueIdForDriver(driverId) : null; const leagueRank = driverId && primaryLeagueId ? getLeagueRankings(driverId, primaryLeagueId) : null; diff --git a/core/racing/application/index.ts b/core/racing/application/index.ts index d30849c45..38aed80a9 100644 --- a/core/racing/application/index.ts +++ b/core/racing/application/index.ts @@ -19,6 +19,7 @@ export * from './use-cases/GetLeagueDriverSeasonStatsUseCase'; export * from './use-cases/GetAllLeaguesWithCapacityUseCase'; export * from './use-cases/GetAllLeaguesWithCapacityAndScoringUseCase'; export * from './use-cases/GetAllRacesUseCase'; +export * from './use-cases/GetAllRacesPageDataUseCase'; export * from './use-cases/GetTotalRacesUseCase'; export * from './use-cases/ImportRaceResultsApiUseCase'; export * from './use-cases/ListLeagueScoringPresetsUseCase'; @@ -74,6 +75,8 @@ export type { } from './dto/LeagueScheduleDTO'; export type { ChampionshipStandingsOutputPort } from './ports/output/ChampionshipStandingsOutputPort'; export type { ChampionshipStandingsRowOutputPort } from './ports/output/ChampionshipStandingsRowOutputPort'; +export type { AllRacesPageOutputPort } from './ports/output/AllRacesPageOutputPort'; +export type { DriverRegistrationStatusOutputPort } from './ports/output/DriverRegistrationStatusOutputPort'; export type { LeagueConfigFormModel, LeagueStructureFormDTO, diff --git a/core/racing/application/ports/output/AllLeaguesWithCapacityAndScoringOutputPort.ts b/core/racing/application/ports/output/AllLeaguesWithCapacityAndScoringOutputPort.ts new file mode 100644 index 000000000..87eee4261 --- /dev/null +++ b/core/racing/application/ports/output/AllLeaguesWithCapacityAndScoringOutputPort.ts @@ -0,0 +1,18 @@ +import type { League } from '../../../domain/entities/League'; +import type { Season } from '../../../domain/entities/season/Season'; +import type { LeagueScoringConfig } from '../../../domain/entities/LeagueScoringConfig'; +import type { Game } from '../../../domain/entities/Game'; +import type { LeagueScoringPresetOutputPort } from './LeagueScoringPresetOutputPort'; + +export interface LeagueEnrichedData { + league: League; + usedDriverSlots: number; + season?: Season; + scoringConfig?: LeagueScoringConfig; + game?: Game; + preset?: LeagueScoringPresetOutputPort; +} + +export interface AllLeaguesWithCapacityAndScoringOutputPort { + leagues: LeagueEnrichedData[]; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/AllLeaguesWithCapacityOutputPort.ts b/core/racing/application/ports/output/AllLeaguesWithCapacityOutputPort.ts new file mode 100644 index 000000000..2fedf476b --- /dev/null +++ b/core/racing/application/ports/output/AllLeaguesWithCapacityOutputPort.ts @@ -0,0 +1,6 @@ +import type { League } from '../../domain/entities/League'; + +export interface AllLeaguesWithCapacityOutputPort { + leagues: League[]; + memberCounts: Record; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/AllRacesPageOutputPort.ts b/core/racing/application/ports/output/AllRacesPageOutputPort.ts new file mode 100644 index 000000000..29722bf6f --- /dev/null +++ b/core/racing/application/ports/output/AllRacesPageOutputPort.ts @@ -0,0 +1,22 @@ +export type AllRacesStatus = 'scheduled' | 'running' | 'completed' | 'cancelled' | 'all'; + +export interface AllRacesListItem { + id: string; + track: string; + car: string; + scheduledAt: string; + status: 'scheduled' | 'running' | 'completed' | 'cancelled'; + leagueId: string; + leagueName: string; + strengthOfField: number | null; +} + +export interface AllRacesFilterOptions { + statuses: { value: AllRacesStatus; label: string }[]; + leagues: { id: string; name: string }[]; +} + +export interface AllRacesPageOutputPort { + races: AllRacesListItem[]; + filters: AllRacesFilterOptions; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/ApproveLeagueJoinRequestOutputPort.ts b/core/racing/application/ports/output/ApproveLeagueJoinRequestOutputPort.ts new file mode 100644 index 000000000..c9f8f1673 --- /dev/null +++ b/core/racing/application/ports/output/ApproveLeagueJoinRequestOutputPort.ts @@ -0,0 +1,4 @@ +export interface ApproveLeagueJoinRequestOutputPort { + success: boolean; + message: string; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/CompleteDriverOnboardingOutputPort.ts b/core/racing/application/ports/output/CompleteDriverOnboardingOutputPort.ts new file mode 100644 index 000000000..2d84a56a1 --- /dev/null +++ b/core/racing/application/ports/output/CompleteDriverOnboardingOutputPort.ts @@ -0,0 +1,3 @@ +export interface CompleteDriverOnboardingOutputPort { + driverId: string; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/CreateLeagueOutputPort.ts b/core/racing/application/ports/output/CreateLeagueOutputPort.ts new file mode 100644 index 000000000..be4f7f99a --- /dev/null +++ b/core/racing/application/ports/output/CreateLeagueOutputPort.ts @@ -0,0 +1,4 @@ +export interface CreateLeagueOutputPort { + leagueId: string; + success: boolean; +} \ No newline at end of file diff --git a/core/racing/application/presenters/IDashboardOverviewPresenter.ts b/core/racing/application/ports/output/DashboardOverviewOutputPort.ts similarity index 51% rename from core/racing/application/presenters/IDashboardOverviewPresenter.ts rename to core/racing/application/ports/output/DashboardOverviewOutputPort.ts index 1c5c9f8a2..4a3bff1c7 100644 --- a/core/racing/application/presenters/IDashboardOverviewPresenter.ts +++ b/core/racing/application/ports/output/DashboardOverviewOutputPort.ts @@ -1,7 +1,6 @@ -import type { Presenter } from '@core/shared/presentation'; import type { FeedItemType } from '@core/social/domain/types/FeedItemType'; -export interface DashboardDriverSummaryViewModel { +export interface DashboardDriverSummaryOutputPort { id: string; name: string; country: string; @@ -14,7 +13,7 @@ export interface DashboardDriverSummaryViewModel { consistency: number | null; } -export interface DashboardRaceSummaryViewModel { +export interface DashboardRaceSummaryOutputPort { id: string; leagueId: string; leagueName: string; @@ -25,7 +24,7 @@ export interface DashboardRaceSummaryViewModel { isMyLeague: boolean; } -export interface DashboardRecentResultViewModel { +export interface DashboardRecentResultOutputPort { raceId: string; raceName: string; leagueId: string; @@ -35,7 +34,7 @@ export interface DashboardRecentResultViewModel { incidents: number; } -export interface DashboardLeagueStandingSummaryViewModel { +export interface DashboardLeagueStandingSummaryOutputPort { leagueId: string; leagueName: string; position: number; @@ -43,7 +42,7 @@ export interface DashboardLeagueStandingSummaryViewModel { points: number; } -export interface DashboardFeedItemSummaryViewModel { +export interface DashboardFeedItemSummaryOutputPort { id: string; type: FeedItemType; headline: string; @@ -53,39 +52,34 @@ export interface DashboardFeedItemSummaryViewModel { ctaHref?: string; } -export interface DashboardFeedSummaryViewModel { +export interface DashboardFeedSummaryOutputPort { notificationCount: number; - items: DashboardFeedItemSummaryViewModel[]; + items: DashboardFeedItemSummaryOutputPort[]; } -export interface DashboardFriendSummaryViewModel { +export interface DashboardFriendSummaryOutputPort { id: string; name: string; country: string; avatarUrl: string; } -export interface DashboardOverviewViewModel { - currentDriver: DashboardDriverSummaryViewModel | null; - myUpcomingRaces: DashboardRaceSummaryViewModel[]; - otherUpcomingRaces: DashboardRaceSummaryViewModel[]; +export interface DashboardOverviewOutputPort { + currentDriver: DashboardDriverSummaryOutputPort | null; + myUpcomingRaces: DashboardRaceSummaryOutputPort[]; + otherUpcomingRaces: DashboardRaceSummaryOutputPort[]; /** * All upcoming races for the driver, already sorted by scheduledAt ascending. */ - upcomingRaces: DashboardRaceSummaryViewModel[]; + upcomingRaces: DashboardRaceSummaryOutputPort[]; /** * Count of distinct leagues that are currently "active" for the driver, * based on upcoming races and league standings. */ activeLeaguesCount: number; - nextRace: DashboardRaceSummaryViewModel | null; - recentResults: DashboardRecentResultViewModel[]; - leagueStandingsSummaries: DashboardLeagueStandingSummaryViewModel[]; - feedSummary: DashboardFeedSummaryViewModel; - friends: DashboardFriendSummaryViewModel[]; -} - -export type DashboardOverviewResultDTO = DashboardOverviewViewModel; - -export interface IDashboardOverviewPresenter - extends Presenter {} \ No newline at end of file + nextRace: DashboardRaceSummaryOutputPort | null; + recentResults: DashboardRecentResultOutputPort[]; + leagueStandingsSummaries: DashboardLeagueStandingSummaryOutputPort[]; + feedSummary: DashboardFeedSummaryOutputPort; + friends: DashboardFriendSummaryOutputPort[]; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/DriverRegistrationStatusOutputPort.ts b/core/racing/application/ports/output/DriverRegistrationStatusOutputPort.ts new file mode 100644 index 000000000..bab15b1d3 --- /dev/null +++ b/core/racing/application/ports/output/DriverRegistrationStatusOutputPort.ts @@ -0,0 +1,5 @@ +export interface DriverRegistrationStatusOutputPort { + isRegistered: boolean; + raceId: string; + driverId: string; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/GetDriverTeamOutputPort.ts b/core/racing/application/ports/output/DriverTeamOutputPort.ts similarity index 62% rename from core/racing/application/ports/output/GetDriverTeamOutputPort.ts rename to core/racing/application/ports/output/DriverTeamOutputPort.ts index c1a887e69..d266cbfc6 100644 --- a/core/racing/application/ports/output/GetDriverTeamOutputPort.ts +++ b/core/racing/application/ports/output/DriverTeamOutputPort.ts @@ -1,4 +1,5 @@ -export interface GetDriverTeamOutputPort { +export interface DriverTeamOutputPort { + driverId: string; team: { id: string; name: string; @@ -9,9 +10,10 @@ export interface GetDriverTeamOutputPort { createdAt: Date; }; membership: { - driverId: string; teamId: string; - role: 'member' | 'captain' | 'admin'; + driverId: string; + role: 'owner' | 'manager' | 'driver'; + status: 'active' | 'pending' | 'none'; joinedAt: Date; }; } \ No newline at end of file diff --git a/core/racing/application/ports/output/DriversLeaderboardOutputPort.ts b/core/racing/application/ports/output/DriversLeaderboardOutputPort.ts new file mode 100644 index 000000000..a464d0cc7 --- /dev/null +++ b/core/racing/application/ports/output/DriversLeaderboardOutputPort.ts @@ -0,0 +1,22 @@ +import type { SkillLevel } from '../../domain/services/SkillLevelService'; + +export interface DriverLeaderboardItemOutputPort { + id: string; + name: string; + rating: number; + skillLevel: SkillLevel; + nationality: string; + racesCompleted: number; + wins: number; + podiums: number; + isActive: boolean; + rank: number; + avatarUrl: string; +} + +export interface DriversLeaderboardOutputPort { + drivers: DriverLeaderboardItemOutputPort[]; + totalRaces: number; + totalWins: number; + activeCount: number; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/GetAllRacesOutputPort.ts b/core/racing/application/ports/output/GetAllRacesOutputPort.ts new file mode 100644 index 000000000..3ef247248 --- /dev/null +++ b/core/racing/application/ports/output/GetAllRacesOutputPort.ts @@ -0,0 +1,13 @@ +export interface GetAllRacesOutputPort { + races: { + id: string; + leagueId: string; + track: string; + car: string; + status: 'scheduled' | 'running' | 'completed' | 'cancelled'; + scheduledAt: string; + strengthOfField: number | null; + leagueName: string; + }[]; + totalCount: number; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/GetAllTeamsOutputPort.ts b/core/racing/application/ports/output/GetAllTeamsOutputPort.ts index 101874d3b..b4a43a655 100644 --- a/core/racing/application/ports/output/GetAllTeamsOutputPort.ts +++ b/core/racing/application/ports/output/GetAllTeamsOutputPort.ts @@ -7,5 +7,7 @@ export interface GetAllTeamsOutputPort { ownerId: string; leagues: string[]; createdAt: Date; + memberCount: number; }>; + totalCount?: number; } \ No newline at end of file diff --git a/core/racing/application/ports/output/GetLeagueAdminOutputPort.ts b/core/racing/application/ports/output/GetLeagueAdminOutputPort.ts index 6d8338113..f1cdc7c48 100644 --- a/core/racing/application/ports/output/GetLeagueAdminOutputPort.ts +++ b/core/racing/application/ports/output/GetLeagueAdminOutputPort.ts @@ -1,7 +1,4 @@ export interface GetLeagueAdminOutputPort { - league: { - id: string; - ownerId: string; - }; - // Additional data would be populated by combining multiple use cases + leagueId: string; + ownerId: string; } \ No newline at end of file diff --git a/core/racing/application/ports/output/GetLeagueJoinRequestsOutputPort.ts b/core/racing/application/ports/output/GetLeagueJoinRequestsOutputPort.ts index 7a4e5bafa..4a9a929a5 100644 --- a/core/racing/application/ports/output/GetLeagueJoinRequestsOutputPort.ts +++ b/core/racing/application/ports/output/GetLeagueJoinRequestsOutputPort.ts @@ -1,13 +1,12 @@ +export interface LeagueJoinRequestOutputPort { + id: string; + leagueId: string; + driverId: string; + requestedAt: Date; + message: string; + driver: { id: string; name: string } | null; +} + export interface GetLeagueJoinRequestsOutputPort { - joinRequests: Array<{ - id: string; - leagueId: string; - driverId: string; - requestedAt: Date; - message?: string; - driver: { - id: string; - name: string; - }; - }>; + joinRequests: LeagueJoinRequestOutputPort[]; } \ No newline at end of file diff --git a/core/racing/application/ports/output/GetLeagueMembershipsOutputPort.ts b/core/racing/application/ports/output/GetLeagueMembershipsOutputPort.ts index 17f9169f9..7b27d3ee9 100644 --- a/core/racing/application/ports/output/GetLeagueMembershipsOutputPort.ts +++ b/core/racing/application/ports/output/GetLeagueMembershipsOutputPort.ts @@ -1,10 +1,14 @@ +export interface LeagueMembershipOutputPort { + driverId: string; + driver: { id: string; name: string }; + role: string; + joinedAt: Date; +} + +export interface LeagueMembershipsOutputPort { + members: LeagueMembershipOutputPort[]; +} + export interface GetLeagueMembershipsOutputPort { - memberships: Array<{ - id: string; - leagueId: string; - driverId: string; - role: 'member' | 'admin' | 'owner'; - joinedAt: Date; - }>; - drivers: { id: string; name: string }[]; + memberships: LeagueMembershipsOutputPort; } \ No newline at end of file diff --git a/core/racing/application/ports/output/GetLeagueOwnerSummaryOutputPort.ts b/core/racing/application/ports/output/GetLeagueOwnerSummaryOutputPort.ts index 320db097c..ac24498b5 100644 --- a/core/racing/application/ports/output/GetLeagueOwnerSummaryOutputPort.ts +++ b/core/racing/application/ports/output/GetLeagueOwnerSummaryOutputPort.ts @@ -1,3 +1,9 @@ +export interface LeagueOwnerSummaryOutputPort { + driver: { id: string; iracingId: string; name: string; country: string; bio: string | undefined; joinedAt: string }; + rating: number; + rank: number; +} + export interface GetLeagueOwnerSummaryOutputPort { - summary: { driver: { id: string; name: string }; rating: number; rank: number } | null; + summary: LeagueOwnerSummaryOutputPort | null; } \ No newline at end of file diff --git a/core/racing/application/ports/output/GetLeagueProtestsOutputPort.ts b/core/racing/application/ports/output/GetLeagueProtestsOutputPort.ts index 69559491e..f8642431d 100644 --- a/core/racing/application/ports/output/GetLeagueProtestsOutputPort.ts +++ b/core/racing/application/ports/output/GetLeagueProtestsOutputPort.ts @@ -1,20 +1,47 @@ +export interface ProtestOutputPort { + id: string; + raceId: string; + protestingDriverId: string; + accusedDriverId: string; + incident: { lap: number; description: string; timeInRace: number | undefined }; + comment: string | undefined; + proofVideoUrl: string | undefined; + status: string; + reviewedBy: string | undefined; + decisionNotes: string | undefined; + filedAt: string; + reviewedAt: string | undefined; + defense: { statement: string; videoUrl: string | undefined; submittedAt: string } | undefined; + defenseRequestedAt: string | undefined; + defenseRequestedBy: string | undefined; +} + +export interface RaceOutputPort { + id: string; + leagueId: string; + scheduledAt: string; + track: string; + trackId: string | undefined; + car: string; + carId: string | undefined; + sessionType: string; + status: string; + strengthOfField: number | undefined; + registeredCount: number | undefined; + maxParticipants: number | undefined; +} + +export interface DriverOutputPort { + id: string; + iracingId: string; + name: string; + country: string; + bio: string | undefined; + joinedAt: string; +} + export interface GetLeagueProtestsOutputPort { - protests: Array<{ - id: string; - raceId: string; - protestingDriverId: string; - accusedDriverId: string; - submittedAt: Date; - description: string; - status: string; - }>; - races: Array<{ - id: string; - name: string; - date: string; - }>; - drivers: Array<{ - id: string; - name: string; - }>; + protests: ProtestOutputPort[]; + racesById: Record; + driversById: Record; } \ No newline at end of file diff --git a/core/racing/application/ports/output/GetLeagueSeasonsOutputPort.ts b/core/racing/application/ports/output/GetLeagueSeasonsOutputPort.ts new file mode 100644 index 000000000..f59aefdec --- /dev/null +++ b/core/racing/application/ports/output/GetLeagueSeasonsOutputPort.ts @@ -0,0 +1,13 @@ +export interface LeagueSeasonSummaryOutputPort { + seasonId: string; + name: string; + status: string; + startDate: Date; + endDate: Date; + isPrimary: boolean; + isParallelActive: boolean; +} + +export interface GetLeagueSeasonsOutputPort { + seasons: LeagueSeasonSummaryOutputPort[]; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/GetSponsorsOutputPort.ts b/core/racing/application/ports/output/GetSponsorsOutputPort.ts new file mode 100644 index 000000000..46996292c --- /dev/null +++ b/core/racing/application/ports/output/GetSponsorsOutputPort.ts @@ -0,0 +1,10 @@ +export interface GetSponsorsOutputPort { + sponsors: { + id: string; + name: string; + contactEmail: string; + websiteUrl: string | undefined; + logoUrl: string | undefined; + createdAt: Date; + }[]; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/GetSponsorshipPricingOutputPort.ts b/core/racing/application/ports/output/GetSponsorshipPricingOutputPort.ts new file mode 100644 index 000000000..f2d67d979 --- /dev/null +++ b/core/racing/application/ports/output/GetSponsorshipPricingOutputPort.ts @@ -0,0 +1,10 @@ +export interface GetSponsorshipPricingOutputPort { + entityType: string; + entityId: string; + pricing: { + id: string; + level: string; + price: number; + currency: string; + }[]; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/GetTeamDetailsOutputPort.ts b/core/racing/application/ports/output/GetTeamDetailsOutputPort.ts index 7a5998c45..1e87505ab 100644 --- a/core/racing/application/ports/output/GetTeamDetailsOutputPort.ts +++ b/core/racing/application/ports/output/GetTeamDetailsOutputPort.ts @@ -9,9 +9,9 @@ export interface GetTeamDetailsOutputPort { createdAt: Date; }; membership: { - driverId: string; - teamId: string; - role: 'member' | 'captain' | 'admin'; + role: 'owner' | 'manager' | 'member'; joinedAt: Date; + isActive: boolean; } | null; + canManage: boolean; } \ No newline at end of file diff --git a/core/racing/application/ports/output/GetTotalLeaguesOutputPort.ts b/core/racing/application/ports/output/GetTotalLeaguesOutputPort.ts new file mode 100644 index 000000000..d98c204cc --- /dev/null +++ b/core/racing/application/ports/output/GetTotalLeaguesOutputPort.ts @@ -0,0 +1,3 @@ +export interface GetTotalLeaguesOutputPort { + totalLeagues: number; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/GetTotalRacesOutputPort.ts b/core/racing/application/ports/output/GetTotalRacesOutputPort.ts new file mode 100644 index 000000000..e93145ff9 --- /dev/null +++ b/core/racing/application/ports/output/GetTotalRacesOutputPort.ts @@ -0,0 +1,3 @@ +export interface GetTotalRacesOutputPort { + totalRaces: number; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/ImportRaceResultsApiOutputPort.ts b/core/racing/application/ports/output/ImportRaceResultsApiOutputPort.ts new file mode 100644 index 000000000..cede2a043 --- /dev/null +++ b/core/racing/application/ports/output/ImportRaceResultsApiOutputPort.ts @@ -0,0 +1,8 @@ +export interface ImportRaceResultsApiOutputPort { + success: boolean; + raceId: string; + leagueId: string; + driversProcessed: number; + resultsRecorded: number; + errors?: string[]; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/JoinLeagueOutputPort.ts b/core/racing/application/ports/output/JoinLeagueOutputPort.ts new file mode 100644 index 000000000..d1dad72c1 --- /dev/null +++ b/core/racing/application/ports/output/JoinLeagueOutputPort.ts @@ -0,0 +1,5 @@ +export interface JoinLeagueOutputPort { + membershipId: string; + leagueId: string; + status: string; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/LeagueDriverSeasonStatsOutputPort.ts b/core/racing/application/ports/output/LeagueDriverSeasonStatsOutputPort.ts index 3edff3ce1..597445c9d 100644 --- a/core/racing/application/ports/output/LeagueDriverSeasonStatsOutputPort.ts +++ b/core/racing/application/ports/output/LeagueDriverSeasonStatsOutputPort.ts @@ -1,4 +1,4 @@ -export interface LeagueDriverSeasonStatsOutputPort { +export interface LeagueDriverSeasonStatsItemOutputPort { leagueId: string; driverId: string; position: number; @@ -17,4 +17,9 @@ export interface LeagueDriverSeasonStatsOutputPort { avgFinish: number | null; rating: number | null; ratingChange: number | null; +} + +export interface LeagueDriverSeasonStatsOutputPort { + leagueId: string; + stats: LeagueDriverSeasonStatsItemOutputPort[]; } \ No newline at end of file diff --git a/core/racing/application/ports/output/LeagueFullConfigOutputPort.ts b/core/racing/application/ports/output/LeagueFullConfigOutputPort.ts new file mode 100644 index 000000000..875f2324b --- /dev/null +++ b/core/racing/application/ports/output/LeagueFullConfigOutputPort.ts @@ -0,0 +1,11 @@ +import type { League } from '../../domain/entities/League'; +import type { Season } from '../../domain/entities/Season'; +import type { LeagueScoringConfig } from '../../domain/entities/LeagueScoringConfig'; +import type { Game } from '../../domain/entities/Game'; + +export interface LeagueFullConfigOutputPort { + league: League; + activeSeason?: Season; + scoringConfig?: LeagueScoringConfig; + game?: Game; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/LeagueScoringConfigOutputPort.ts b/core/racing/application/ports/output/LeagueScoringConfigOutputPort.ts index 7983bbcf4..05210b338 100644 --- a/core/racing/application/ports/output/LeagueScoringConfigOutputPort.ts +++ b/core/racing/application/ports/output/LeagueScoringConfigOutputPort.ts @@ -1,18 +1,12 @@ +import type { ChampionshipConfig } from '../../domain/types/ChampionshipConfig'; +import type { LeagueScoringPresetOutputPort } from './LeagueScoringPresetOutputPort'; + export interface LeagueScoringConfigOutputPort { leagueId: string; seasonId: string; gameId: string; gameName: string; scoringPresetId?: string; - scoringPresetName?: string; - dropPolicySummary: string; - championships: Array<{ - id: string; - name: string; - type: 'driver' | 'team' | 'nations' | 'trophy'; - sessionTypes: string[]; - pointsPreview: Array<{ sessionType: string; position: number; points: number }>; - bonusSummary: string[]; - dropPolicyDescription: string; - }>; + preset?: LeagueScoringPresetOutputPort; + championships: ChampionshipConfig[]; } \ No newline at end of file diff --git a/core/racing/application/ports/output/LeagueScoringPresetsOutputPort.ts b/core/racing/application/ports/output/LeagueScoringPresetsOutputPort.ts new file mode 100644 index 000000000..140de4a63 --- /dev/null +++ b/core/racing/application/ports/output/LeagueScoringPresetsOutputPort.ts @@ -0,0 +1,5 @@ +import type { LeagueScoringPresetOutputPort } from './LeagueScoringPresetOutputPort'; + +export interface LeagueScoringPresetsOutputPort { + presets: LeagueScoringPresetOutputPort[]; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/LeagueStandingsOutputPort.ts b/core/racing/application/ports/output/LeagueStandingsOutputPort.ts new file mode 100644 index 000000000..f47179891 --- /dev/null +++ b/core/racing/application/ports/output/LeagueStandingsOutputPort.ts @@ -0,0 +1,10 @@ +export interface StandingItemOutputPort { + driverId: string; + driver: { id: string; name: string }; + points: number; + rank: number; +} + +export interface LeagueStandingsOutputPort { + standings: StandingItemOutputPort[]; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/LeagueStatsOutputPort.ts b/core/racing/application/ports/output/LeagueStatsOutputPort.ts new file mode 100644 index 000000000..898aded6a --- /dev/null +++ b/core/racing/application/ports/output/LeagueStatsOutputPort.ts @@ -0,0 +1,5 @@ +export interface LeagueStatsOutputPort { + totalMembers: number; + totalRaces: number; + averageRating: number; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/PendingSponsorshipRequestsOutputPort.ts b/core/racing/application/ports/output/PendingSponsorshipRequestsOutputPort.ts new file mode 100644 index 000000000..4089b7348 --- /dev/null +++ b/core/racing/application/ports/output/PendingSponsorshipRequestsOutputPort.ts @@ -0,0 +1,24 @@ +import type { SponsorableEntityType } from '../../domain/entities/SponsorshipRequest'; +import type { SponsorshipTier } from '../../domain/entities/SeasonSponsorship'; + +export interface PendingSponsorshipRequestOutput { + id: string; + sponsorId: string; + sponsorName: string; + sponsorLogo?: string; + tier: SponsorshipTier; + offeredAmount: number; + currency: string; + formattedAmount: string; + message?: string; + createdAt: Date; + platformFee: number; + netAmount: number; +} + +export interface PendingSponsorshipRequestsOutputPort { + entityType: SponsorableEntityType; + entityId: string; + requests: PendingSponsorshipRequestOutput[]; + totalCount: number; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/ProfileOverviewOutputPort.ts b/core/racing/application/ports/output/ProfileOverviewOutputPort.ts new file mode 100644 index 000000000..cc2514d1c --- /dev/null +++ b/core/racing/application/ports/output/ProfileOverviewOutputPort.ts @@ -0,0 +1,57 @@ +export interface ProfileOverviewOutputPort { + driver: { + id: string; + name: string; + country: string; + avatarUrl: string; + iracingId: string | null; + joinedAt: Date; + rating: number | null; + globalRank: number | null; + consistency: number | null; + bio: string | null; + totalDrivers: number | null; + }; + stats: { + totalRaces: number; + wins: number; + podiums: number; + dnfs: number; + avgFinish: number | null; + bestFinish: number | null; + worstFinish: number | null; + finishRate: number | null; + winRate: number | null; + podiumRate: number | null; + percentile: number | null; + rating: number | null; + consistency: number | null; + overallRank: number | null; + } | null; + finishDistribution: { + totalRaces: number; + wins: number; + podiums: number; + topTen: number; + dnfs: number; + other: number; + } | null; + teamMemberships: { + teamId: string; + teamName: string; + teamTag: string | null; + role: string; + joinedAt: Date; + isCurrent: boolean; + }[]; + socialSummary: { + friendsCount: number; + friends: { + id: string; + name: string; + country: string; + avatarUrl: string; + }[]; + }; + extendedProfile: null; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/RaceDetailOutputPort.ts b/core/racing/application/ports/output/RaceDetailOutputPort.ts new file mode 100644 index 000000000..ae20730b9 --- /dev/null +++ b/core/racing/application/ports/output/RaceDetailOutputPort.ts @@ -0,0 +1,15 @@ +import type { Race } from '../../../domain/entities/Race'; +import type { League } from '../../../domain/entities/League'; +import type { RaceRegistration } from '../../../domain/entities/RaceRegistration'; +import type { Driver } from '../../../domain/entities/Driver'; +import type { Result } from '../../../domain/entities/result/Result'; + +export interface RaceDetailOutputPort { + race: Race; + league: League | null; + registrations: RaceRegistration[]; + drivers: Driver[]; + userResult: Result | null; + isUserRegistered: boolean; + canRegister: boolean; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/RacePenaltiesOutputPort.ts b/core/racing/application/ports/output/RacePenaltiesOutputPort.ts new file mode 100644 index 000000000..48c02c8f7 --- /dev/null +++ b/core/racing/application/ports/output/RacePenaltiesOutputPort.ts @@ -0,0 +1,7 @@ +import type { Penalty } from '../../../domain/entities/Penalty'; +import type { Driver } from '../../../domain/entities/Driver'; + +export interface RacePenaltiesOutputPort { + penalties: Penalty[]; + drivers: Driver[]; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/RaceProtestsOutputPort.ts b/core/racing/application/ports/output/RaceProtestsOutputPort.ts new file mode 100644 index 000000000..9fc2f9c2b --- /dev/null +++ b/core/racing/application/ports/output/RaceProtestsOutputPort.ts @@ -0,0 +1,7 @@ +import type { Protest } from '../../../domain/entities/Protest'; +import type { Driver } from '../../../domain/entities/Driver'; + +export interface RaceProtestsOutputPort { + protests: Protest[]; + drivers: Driver[]; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/RaceRegistrationsOutputPort.ts b/core/racing/application/ports/output/RaceRegistrationsOutputPort.ts new file mode 100644 index 000000000..dca2d6f9c --- /dev/null +++ b/core/racing/application/ports/output/RaceRegistrationsOutputPort.ts @@ -0,0 +1,5 @@ +import type { RaceRegistration } from '../../../domain/entities/RaceRegistration'; + +export interface RaceRegistrationsOutputPort { + registrations: RaceRegistration[]; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/RaceResultsDetailOutputPort.ts b/core/racing/application/ports/output/RaceResultsDetailOutputPort.ts new file mode 100644 index 000000000..378c9ed88 --- /dev/null +++ b/core/racing/application/ports/output/RaceResultsDetailOutputPort.ts @@ -0,0 +1,16 @@ +import type { Race } from '../../../domain/entities/Race'; +import type { League } from '../../../domain/entities/League'; +import type { Result } from '../../../domain/entities/result/Result'; +import type { Driver } from '../../../domain/entities/Driver'; +import type { Penalty } from '../../../domain/entities/Penalty'; + +export interface RaceResultsDetailOutputPort { + race: Race; + league: League | null; + results: Result[]; + drivers: Driver[]; + penalties: Penalty[]; + pointsSystem?: Record; + fastestLapTime?: number; + currentDriverId?: string; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/RaceWithSOFOutputPort.ts b/core/racing/application/ports/output/RaceWithSOFOutputPort.ts new file mode 100644 index 000000000..82b85bbe8 --- /dev/null +++ b/core/racing/application/ports/output/RaceWithSOFOutputPort.ts @@ -0,0 +1,12 @@ +export interface RaceWithSOFOutputPort { + id: string; + leagueId: string; + track: string; + car: string; + scheduledAt: Date; + status: 'scheduled' | 'running' | 'completed' | 'cancelled'; + strengthOfField: number | null; + registeredCount: number; + maxParticipants: number; + participantCount: number; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/RacesPageOutputPort.ts b/core/racing/application/ports/output/RacesPageOutputPort.ts new file mode 100644 index 000000000..d7e63ac22 --- /dev/null +++ b/core/racing/application/ports/output/RacesPageOutputPort.ts @@ -0,0 +1,14 @@ +export interface RacesPageOutputPort { + page: number; + pageSize: number; + totalCount: number; + races: { + id: string; + leagueId: string; + track: string; + car: string; + scheduledAt: Date; + status: 'scheduled' | 'running' | 'completed' | 'cancelled'; + strengthOfField: number | null; + }[]; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/RejectLeagueJoinRequestOutputPort.ts b/core/racing/application/ports/output/RejectLeagueJoinRequestOutputPort.ts new file mode 100644 index 000000000..02eca0b8a --- /dev/null +++ b/core/racing/application/ports/output/RejectLeagueJoinRequestOutputPort.ts @@ -0,0 +1,4 @@ +export interface RejectLeagueJoinRequestOutputPort { + success: boolean; + message: string; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/RemoveLeagueMemberOutputPort.ts b/core/racing/application/ports/output/RemoveLeagueMemberOutputPort.ts new file mode 100644 index 000000000..88f9827ba --- /dev/null +++ b/core/racing/application/ports/output/RemoveLeagueMemberOutputPort.ts @@ -0,0 +1,3 @@ +export interface RemoveLeagueMemberOutputPort { + success: boolean; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/SponsorDashboardOutputPort.ts b/core/racing/application/ports/output/SponsorDashboardOutputPort.ts new file mode 100644 index 000000000..2e4a85b0e --- /dev/null +++ b/core/racing/application/ports/output/SponsorDashboardOutputPort.ts @@ -0,0 +1,30 @@ +export interface SponsoredLeagueOutput { + id: string; + name: string; + tier: 'main' | 'secondary'; + drivers: number; + races: number; + impressions: number; + status: 'active' | 'upcoming' | 'completed'; +} + +export interface SponsorDashboardOutputPort { + sponsorId: string; + sponsorName: string; + metrics: { + impressions: number; + impressionsChange: number; + uniqueViewers: number; + viewersChange: number; + races: number; + drivers: number; + exposure: number; + exposureChange: number; + }; + sponsoredLeagues: SponsoredLeagueOutput[]; + investment: { + activeSponsorships: number; + totalInvestment: number; + costPerThousandViews: number; + }; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/SponsorSponsorshipsOutputPort.ts b/core/racing/application/ports/output/SponsorSponsorshipsOutputPort.ts new file mode 100644 index 000000000..727a77a84 --- /dev/null +++ b/core/racing/application/ports/output/SponsorSponsorshipsOutputPort.ts @@ -0,0 +1,46 @@ +import type { SponsorshipTier, SponsorshipStatus } from '../../domain/entities/SeasonSponsorship'; + +export interface SponsorshipDetailOutput { + id: string; + leagueId: string; + leagueName: string; + seasonId: string; + seasonName: string; + seasonStartDate?: Date; + seasonEndDate?: Date; + tier: SponsorshipTier; + status: SponsorshipStatus; + pricing: { + amount: number; + currency: string; + }; + platformFee: { + amount: number; + currency: string; + }; + netAmount: { + amount: number; + currency: string; + }; + metrics: { + drivers: number; + races: number; + completedRaces: number; + impressions: number; + }; + createdAt: Date; + activatedAt?: Date; +} + +export interface SponsorSponsorshipsOutputPort { + sponsorId: string; + sponsorName: string; + sponsorships: SponsorshipDetailOutput[]; + summary: { + totalSponsorships: number; + activeSponsorships: number; + totalInvestment: number; + totalPlatformFees: number; + currency: string; + }; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/TeamJoinRequestsOutputPort.ts b/core/racing/application/ports/output/TeamJoinRequestsOutputPort.ts new file mode 100644 index 000000000..b564be6a7 --- /dev/null +++ b/core/racing/application/ports/output/TeamJoinRequestsOutputPort.ts @@ -0,0 +1,13 @@ +export interface TeamJoinRequestsOutputPort { + requests: { + requestId: string; + driverId: string; + driverName: string; + teamId: string; + status: 'pending' | 'approved' | 'rejected'; + requestedAt: Date; + avatarUrl: string; + }[]; + pendingCount: number; + totalCount: number; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/TeamMembersOutputPort.ts b/core/racing/application/ports/output/TeamMembersOutputPort.ts new file mode 100644 index 000000000..a3b79e93b --- /dev/null +++ b/core/racing/application/ports/output/TeamMembersOutputPort.ts @@ -0,0 +1,14 @@ +export interface TeamMembersOutputPort { + members: { + driverId: string; + driverName: string; + role: 'owner' | 'manager' | 'member'; + joinedAt: Date; + isActive: boolean; + avatarUrl: string; + }[]; + totalCount: number; + ownerCount: number; + managerCount: number; + memberCount: number; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/TeamsLeaderboardOutputPort.ts b/core/racing/application/ports/output/TeamsLeaderboardOutputPort.ts new file mode 100644 index 000000000..317ea3dc1 --- /dev/null +++ b/core/racing/application/ports/output/TeamsLeaderboardOutputPort.ts @@ -0,0 +1,50 @@ +export type SkillLevel = 'beginner' | 'intermediate' | 'advanced' | 'pro'; + +export interface TeamsLeaderboardOutputPort { + teams: { + id: string; + name: string; + memberCount: number; + rating: number | null; + totalWins: number; + totalRaces: number; + performanceLevel: SkillLevel; + isRecruiting: boolean; + createdAt: Date; + description?: string; + specialization?: 'endurance' | 'sprint' | 'mixed'; + region?: string; + languages?: string[]; + }[]; + recruitingCount: number; + groupsBySkillLevel: Record; + topTeams: { + id: string; + name: string; + memberCount: number; + rating: number | null; + totalWins: number; + totalRaces: number; + performanceLevel: SkillLevel; + isRecruiting: boolean; + createdAt: Date; + description?: string; + specialization?: 'endurance' | 'sprint' | 'mixed'; + region?: string; + languages?: string[]; + }[]; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/TotalDriversOutputPort.ts b/core/racing/application/ports/output/TotalDriversOutputPort.ts new file mode 100644 index 000000000..9b951472d --- /dev/null +++ b/core/racing/application/ports/output/TotalDriversOutputPort.ts @@ -0,0 +1,3 @@ +export interface TotalDriversOutputPort { + totalDrivers: number; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/TransferLeagueOwnershipOutputPort.ts b/core/racing/application/ports/output/TransferLeagueOwnershipOutputPort.ts new file mode 100644 index 000000000..1fab32f78 --- /dev/null +++ b/core/racing/application/ports/output/TransferLeagueOwnershipOutputPort.ts @@ -0,0 +1,3 @@ +export interface TransferLeagueOwnershipOutputPort { + success: boolean; +} \ No newline at end of file diff --git a/core/racing/application/ports/output/UpdateLeagueMemberRoleOutputPort.ts b/core/racing/application/ports/output/UpdateLeagueMemberRoleOutputPort.ts new file mode 100644 index 000000000..4515359e0 --- /dev/null +++ b/core/racing/application/ports/output/UpdateLeagueMemberRoleOutputPort.ts @@ -0,0 +1,3 @@ +export interface UpdateLeagueMemberRoleOutputPort { + success: boolean; +} \ No newline at end of file diff --git a/core/racing/application/presenters/IAllLeaguesWithCapacityAndScoringPresenter.ts b/core/racing/application/presenters/IAllLeaguesWithCapacityAndScoringPresenter.ts deleted file mode 100644 index c83102d74..000000000 --- a/core/racing/application/presenters/IAllLeaguesWithCapacityAndScoringPresenter.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { League } from '../../domain/entities/League'; -import type { Season } from '../../domain/entities/Season'; -import type { LeagueScoringConfig } from '../../domain/entities/LeagueScoringConfig'; -import type { Game } from '../../domain/entities/Game'; -import type { LeagueScoringPresetOutputPort } from '../ports/output/LeagueScoringPresetOutputPort'; -import type { Presenter } from '@core/shared/presentation'; - -export interface LeagueSummaryViewModel { - id: string; - name: string; - description: string; - ownerId: string; - createdAt: string; - maxDrivers: number; - usedDriverSlots: number; - maxTeams?: number; - usedTeamSlots?: number; - structureSummary: string; - scoringPatternSummary?: string; - timingSummary: string; - scoring?: { - gameId: string; - gameName: string; - primaryChampionshipType: 'driver' | 'team' | 'nations' | 'trophy'; - scoringPresetId: string; - scoringPresetName: string; - dropPolicySummary: string; - scoringPatternSummary: string; - }; -} - -export interface AllLeaguesWithCapacityAndScoringViewModel { - leagues: LeagueSummaryViewModel[]; - totalCount: number; -} - -export interface LeagueEnrichedData { - league: League; - usedDriverSlots: number; - season?: Season; - scoringConfig?: LeagueScoringConfig; - game?: Game; - preset?: LeagueScoringPresetOutputPort; -} - -export interface IAllLeaguesWithCapacityAndScoringPresenter - extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/IAllLeaguesWithCapacityPresenter.ts b/core/racing/application/presenters/IAllLeaguesWithCapacityPresenter.ts deleted file mode 100644 index a46440f39..000000000 --- a/core/racing/application/presenters/IAllLeaguesWithCapacityPresenter.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { League } from '../../domain/entities/League'; -import type { Presenter } from '@core/shared/presentation'; - -export interface LeagueWithCapacityViewModel { - id: string; - name: string; - description: string; - ownerId: string; - settings: { - maxDrivers: number; - sessionDuration?: number; - visibility?: string; - }; - createdAt: string; - socialLinks?: { - discordUrl?: string; - youtubeUrl?: string; - websiteUrl?: string; - }; - usedSlots: number; -} - -export interface AllLeaguesWithCapacityViewModel { - leagues: LeagueWithCapacityViewModel[]; - totalCount: number; -} - -export interface AllLeaguesWithCapacityResultDTO { - leagues: League[]; - memberCounts: Map; -} - -export interface IAllLeaguesWithCapacityPresenter - extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/IAllRacesPagePresenter.ts b/core/racing/application/presenters/IAllRacesPagePresenter.ts deleted file mode 100644 index 468c89c2a..000000000 --- a/core/racing/application/presenters/IAllRacesPagePresenter.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { Presenter } from '@core/shared/presentation'; - -export type AllRacesStatus = 'scheduled' | 'running' | 'completed' | 'cancelled' | 'all'; - -export interface AllRacesListItemViewModel { - id: string; - track: string; - car: string; - scheduledAt: string; - status: 'scheduled' | 'running' | 'completed' | 'cancelled'; - leagueId: string; - leagueName: string; - strengthOfField: number | null; -} - -export interface AllRacesFilterOptionsViewModel { - statuses: { value: AllRacesStatus; label: string }[]; - leagues: { id: string; name: string }[]; -} - -export interface AllRacesPageViewModel { - races: AllRacesListItemViewModel[]; - filters: AllRacesFilterOptionsViewModel; -} - -export type AllRacesPageResultDTO = AllRacesPageViewModel; - -export interface IAllRacesPagePresenter - extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/IAllTeamsPresenter.ts b/core/racing/application/presenters/IAllTeamsPresenter.ts deleted file mode 100644 index 25bddeb2c..000000000 --- a/core/racing/application/presenters/IAllTeamsPresenter.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { Presenter } from '@core/shared/presentation'; - -export interface TeamListItemViewModel { - id: string; - name: string; - tag: string; - description: string; - memberCount: number; - leagues: string[]; - specialization?: 'endurance' | 'sprint' | 'mixed'; - region?: string; - languages?: string[]; -} - -export interface AllTeamsViewModel { - teams: TeamListItemViewModel[]; - totalCount: number; -} - -export interface AllTeamsResultDTO { - teams: Array<{ - id: string; - name: string; - tag: string; - description: string; - ownerId: string; - leagues: string[]; - createdAt: Date; - memberCount: number; - }>; -} - -export interface IAllTeamsPresenter - extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/IApproveLeagueJoinRequestPresenter.ts b/core/racing/application/presenters/IApproveLeagueJoinRequestPresenter.ts deleted file mode 100644 index 40b698fd0..000000000 --- a/core/racing/application/presenters/IApproveLeagueJoinRequestPresenter.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { Presenter } from '@core/shared/presentation/Presenter'; - -export interface ApproveLeagueJoinRequestViewModel { - success: boolean; - message: string; -} - -export interface ApproveLeagueJoinRequestResultPort { - success: boolean; - message: string; -} - -export interface IApproveLeagueJoinRequestPresenter extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/ICompleteDriverOnboardingPresenter.ts b/core/racing/application/presenters/ICompleteDriverOnboardingPresenter.ts deleted file mode 100644 index 92eea9e6d..000000000 --- a/core/racing/application/presenters/ICompleteDriverOnboardingPresenter.ts +++ /dev/null @@ -1,17 +0,0 @@ -export interface CompleteDriverOnboardingViewModel { - success: boolean; - driverId?: string; - errorMessage?: string; -} - -export interface CompleteDriverOnboardingResultDTO { - success: boolean; - driverId?: string; - errorMessage?: string; -} - -export interface ICompleteDriverOnboardingPresenter { - present(dto: CompleteDriverOnboardingResultDTO): void; - get viewModel(): CompleteDriverOnboardingViewModel; - reset(): void; -} \ No newline at end of file diff --git a/core/racing/application/presenters/ICreateLeaguePresenter.ts b/core/racing/application/presenters/ICreateLeaguePresenter.ts deleted file mode 100644 index e2ab191d5..000000000 --- a/core/racing/application/presenters/ICreateLeaguePresenter.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Presenter } from './Presenter'; - -export interface CreateLeagueResultDTO { - leagueId: string; - seasonId: string; - scoringPresetId?: string; - scoringPresetName?: string; -} - -export interface CreateLeagueViewModel { - leagueId: string; - success: boolean; -} - -export interface ICreateLeaguePresenter extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/ICreateSponsorPresenter.ts b/core/racing/application/presenters/ICreateSponsorPresenter.ts deleted file mode 100644 index 1dbbc19c6..000000000 --- a/core/racing/application/presenters/ICreateSponsorPresenter.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Presenter } from '@core/shared/presentation/Presenter'; - -export interface SponsorDto { - id: string; - name: string; - contactEmail: string; - websiteUrl: string | undefined; - logoUrl: string | undefined; - createdAt: Date; -} - -export interface CreateSponsorViewModel { - sponsor: SponsorDto; -} - -export interface CreateSponsorOutputPort { - sponsor: SponsorDto; -} - -export interface ICreateSponsorPresenter extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/IDriverRegistrationStatusPresenter.ts b/core/racing/application/presenters/IDriverRegistrationStatusPresenter.ts deleted file mode 100644 index f4e15d91d..000000000 --- a/core/racing/application/presenters/IDriverRegistrationStatusPresenter.ts +++ /dev/null @@ -1,14 +0,0 @@ -export interface DriverRegistrationStatusViewModel { - isRegistered: boolean; - raceId: string; - driverId: string; -} - -export interface IDriverRegistrationStatusPresenter { - present( - isRegistered: boolean, - raceId: string, - driverId: string - ): DriverRegistrationStatusViewModel; - getViewModel(): DriverRegistrationStatusViewModel; -} \ No newline at end of file diff --git a/core/racing/application/presenters/IDriverTeamPresenter.ts b/core/racing/application/presenters/IDriverTeamPresenter.ts deleted file mode 100644 index 8180cf5bd..000000000 --- a/core/racing/application/presenters/IDriverTeamPresenter.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { Team } from '../../domain/entities/Team'; -import type { TeamMembership } from '../../domain/types/TeamMembership'; -import type { Presenter } from '@core/shared/presentation'; - -export interface DriverTeamViewModel { - team: { - id: string; - name: string; - tag: string; - description: string; - ownerId: string; - leagues: string[]; - specialization?: 'endurance' | 'sprint' | 'mixed'; - region?: string; - languages?: string[]; - }; - membership: { - role: 'owner' | 'manager' | 'member'; - joinedAt: string; - isActive: boolean; - }; - isOwner: boolean; - canManage: boolean; -} - -export interface DriverTeamResultDTO { - team: Team; - membership: TeamMembership; - driverId: string; -} - -export interface IDriverTeamPresenter - extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/IDriversLeaderboardPresenter.ts b/core/racing/application/presenters/IDriversLeaderboardPresenter.ts deleted file mode 100644 index 3d29bdf14..000000000 --- a/core/racing/application/presenters/IDriversLeaderboardPresenter.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { Driver } from '../../domain/entities/Driver'; -import type { SkillLevel } from '../../domain/services/SkillLevelService'; -import type { Presenter } from '@core/shared/presentation'; - -export type { SkillLevel }; - -export interface DriverLeaderboardItemViewModel { - id: string; - name: string; - rating: number; - skillLevel: SkillLevel; - nationality: string; - racesCompleted: number; - wins: number; - podiums: number; - isActive: boolean; - rank: number; - avatarUrl: string; -} - -export interface DriversLeaderboardViewModel { - drivers: DriverLeaderboardItemViewModel[]; - totalRaces: number; - totalWins: number; - activeCount: number; -} - -export interface DriversLeaderboardResultDTO { - drivers: Driver[]; - rankings: Array<{ driverId: string; rating: number; overallRank: number | null }>; - stats: Record< - string, - { - rating: number; - wins: number; - podiums: number; - totalRaces: number; - overallRank: number | null; - } - >; - avatarUrls: Record; -} - -export interface IDriversLeaderboardPresenter - extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/IEntitySponsorshipPricingPresenter.ts b/core/racing/application/presenters/IEntitySponsorshipPricingPresenter.ts deleted file mode 100644 index 58478b233..000000000 --- a/core/racing/application/presenters/IEntitySponsorshipPricingPresenter.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { Presenter } from '@core/shared/presentation/Presenter'; -import type { GetEntitySponsorshipPricingResultDTO } from '../use-cases/GetEntitySponsorshipPricingUseCase'; - -export interface IEntitySponsorshipPricingPresenter extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/IGetAllRacesPresenter.ts b/core/racing/application/presenters/IGetAllRacesPresenter.ts deleted file mode 100644 index 787344717..000000000 --- a/core/racing/application/presenters/IGetAllRacesPresenter.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Presenter } from '@core/shared/presentation/Presenter'; - -export interface RaceViewModel { - id: string; - name: string; - date: string; - leagueName?: string; -} - -export interface AllRacesPageViewModel { - races: RaceViewModel[]; - totalCount: number; -} - -export interface GetAllRacesResultDTO { - races: RaceViewModel[]; - totalCount: number; -} - -export interface IGetAllRacesPresenter extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/IGetLeagueAdminPermissionsPresenter.ts b/core/racing/application/presenters/IGetLeagueAdminPermissionsPresenter.ts deleted file mode 100644 index 1961324f0..000000000 --- a/core/racing/application/presenters/IGetLeagueAdminPermissionsPresenter.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { Presenter } from '@core/shared/presentation/Presenter'; - -export interface GetLeagueAdminPermissionsViewModel { - canRemoveMember: boolean; - canUpdateRoles: boolean; -} - -export interface GetLeagueAdminPermissionsResultDTO { - canRemoveMember: boolean; - canUpdateRoles: boolean; -} - -export interface IGetLeagueAdminPermissionsPresenter extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/IGetLeagueAdminPresenter.ts b/core/racing/application/presenters/IGetLeagueAdminPresenter.ts deleted file mode 100644 index f30e20280..000000000 --- a/core/racing/application/presenters/IGetLeagueAdminPresenter.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { Presenter } from '@core/shared/presentation/Presenter'; - -export interface LeagueAdminViewModel { - leagueId: string; - ownerId: string; -} - -export interface GetLeagueAdminResultDTO { - leagueId: string; - ownerId: string; -} - -export interface IGetLeagueAdminPresenter extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/IGetLeagueJoinRequestsPresenter.ts b/core/racing/application/presenters/IGetLeagueJoinRequestsPresenter.ts deleted file mode 100644 index 8e29128f5..000000000 --- a/core/racing/application/presenters/IGetLeagueJoinRequestsPresenter.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { Presenter } from '@core/shared/presentation/Presenter'; - -export interface LeagueJoinRequestViewModel { - id: string; - leagueId: string; - driverId: string; - requestedAt: Date; - message: string; - driver: { id: string; name: string } | null; -} - -export interface GetLeagueJoinRequestsViewModel { - joinRequests: LeagueJoinRequestViewModel[]; -} - -export interface GetLeagueJoinRequestsResultDTO { - joinRequests: unknown[]; - drivers: { id: string; name: string }[]; -} - -export interface IGetLeagueJoinRequestsPresenter extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/IGetLeagueMembershipsPresenter.ts b/core/racing/application/presenters/IGetLeagueMembershipsPresenter.ts deleted file mode 100644 index a129db10a..000000000 --- a/core/racing/application/presenters/IGetLeagueMembershipsPresenter.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { Presenter } from '@core/shared/presentation/Presenter'; - -export interface LeagueMembershipsViewModel { - members: { - driverId: string; - driver: { id: string; name: string }; - role: string; - joinedAt: Date; - }[]; -} - -export interface GetLeagueMembershipsViewModel { - memberships: LeagueMembershipsViewModel; -} - -export interface GetLeagueMembershipsResultDTO { - memberships: unknown[]; - drivers: { id: string; name: string }[]; -} - -export interface IGetLeagueMembershipsPresenter extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/IGetLeagueOwnerSummaryPresenter.ts b/core/racing/application/presenters/IGetLeagueOwnerSummaryPresenter.ts deleted file mode 100644 index 2ff229564..000000000 --- a/core/racing/application/presenters/IGetLeagueOwnerSummaryPresenter.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Presenter } from '@core/shared/presentation/Presenter'; - -export interface LeagueOwnerSummaryViewModel { - driver: { id: string; name: string }; - rating: number; - rank: number; -} - -export interface GetLeagueOwnerSummaryViewModel { - summary: LeagueOwnerSummaryViewModel | null; -} - -export interface GetLeagueOwnerSummaryResultDTO { - summary: LeagueOwnerSummaryViewModel | null; -} - -export interface IGetLeagueOwnerSummaryPresenter extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/IGetLeagueProtestsPresenter.ts b/core/racing/application/presenters/IGetLeagueProtestsPresenter.ts deleted file mode 100644 index 4f9403a92..000000000 --- a/core/racing/application/presenters/IGetLeagueProtestsPresenter.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { Presenter } from '@core/shared/presentation/Presenter'; - -export interface GetLeagueProtestsViewModel { - protests: unknown[]; - racesById: Record; - driversById: Record; -} - -export interface GetLeagueProtestsResultDTO { - protests: unknown[]; - races: unknown[]; - drivers: { id: string; name: string }[]; -} - -export interface IGetLeagueProtestsPresenter extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/IGetLeagueSchedulePresenter.ts b/core/racing/application/presenters/IGetLeagueSchedulePresenter.ts deleted file mode 100644 index 649c931a5..000000000 --- a/core/racing/application/presenters/IGetLeagueSchedulePresenter.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { Presenter } from '@core/shared/presentation/Presenter'; - -export interface LeagueScheduleViewModel { - races: Array<{ - id: string; - name: string; - date: string; - }>; -} - -export interface GetLeagueScheduleResultDTO { - races: Array<{ - id: string; - name: string; - scheduledAt: Date; - }>; -} - -export interface IGetLeagueSchedulePresenter extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/IGetLeagueSeasonsPresenter.ts b/core/racing/application/presenters/IGetLeagueSeasonsPresenter.ts deleted file mode 100644 index 57ff6e95d..000000000 --- a/core/racing/application/presenters/IGetLeagueSeasonsPresenter.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { Presenter } from '@core/shared/presentation/Presenter'; - -export interface LeagueSeasonSummaryViewModel { - seasonId: string; - name: string; - status: string; - startDate: Date; - endDate: Date; - isPrimary: boolean; - isParallelActive: boolean; -} - -export interface GetLeagueSeasonsViewModel { - seasons: LeagueSeasonSummaryViewModel[]; -} - -export interface GetLeagueSeasonsResultDTO { - seasons: unknown[]; -} - -export interface IGetLeagueSeasonsPresenter extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/IGetSponsorsPresenter.ts b/core/racing/application/presenters/IGetSponsorsPresenter.ts deleted file mode 100644 index 62540e7bc..000000000 --- a/core/racing/application/presenters/IGetSponsorsPresenter.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Presenter } from '@core/shared/presentation/Presenter'; - -export interface SponsorDto { - id: string; - name: string; - contactEmail: string; - websiteUrl: string | undefined; - logoUrl: string | undefined; - createdAt: Date; -} - -export interface GetSponsorsViewModel { - sponsors: SponsorDto[]; -} - -export interface GetSponsorsResultDTO { - sponsors: SponsorDto[]; -} - -export interface IGetSponsorsPresenter extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/IGetSponsorshipPricingPresenter.ts b/core/racing/application/presenters/IGetSponsorshipPricingPresenter.ts deleted file mode 100644 index b61aadf64..000000000 --- a/core/racing/application/presenters/IGetSponsorshipPricingPresenter.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Presenter } from '@core/shared/presentation/Presenter'; - -export interface SponsorshipPricingItemDto { - id: string; - level: string; - price: number; - currency: string; -} - -export interface GetSponsorshipPricingResultDTO { - pricing: SponsorshipPricingItemDto[]; -} - -export interface GetSponsorshipPricingViewModel { - pricing: SponsorshipPricingItemDto[]; -} - -export interface IGetSponsorshipPricingPresenter extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/IGetTotalLeaguesPresenter.ts b/core/racing/application/presenters/IGetTotalLeaguesPresenter.ts deleted file mode 100644 index a2aa44d5c..000000000 --- a/core/racing/application/presenters/IGetTotalLeaguesPresenter.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Presenter } from '@core/shared/presentation/Presenter'; - -export interface GetTotalLeaguesViewModel { - totalLeagues: number; -} - -export interface GetTotalLeaguesResultDTO { - totalLeagues: number; -} - -export interface IGetTotalLeaguesPresenter extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/IGetTotalRacesPresenter.ts b/core/racing/application/presenters/IGetTotalRacesPresenter.ts deleted file mode 100644 index 439cc916d..000000000 --- a/core/racing/application/presenters/IGetTotalRacesPresenter.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Presenter } from '@core/shared/presentation/Presenter'; - -export interface GetTotalRacesViewModel { - totalRaces: number; -} - -export interface GetTotalRacesResultDTO { - totalRaces: number; -} - -export interface IGetTotalRacesPresenter extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/IImportRaceResultsApiPresenter.ts b/core/racing/application/presenters/IImportRaceResultsApiPresenter.ts deleted file mode 100644 index 804eb4788..000000000 --- a/core/racing/application/presenters/IImportRaceResultsApiPresenter.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { Presenter } from '@core/shared/presentation/Presenter'; - -export interface ImportRaceResultsSummaryViewModel { - success: boolean; - raceId: string; - driversProcessed: number; - resultsRecorded: number; - errors?: string[]; -} - -export interface ImportRaceResultsApiResultDTO { - success: boolean; - raceId: string; - driversProcessed: number; - resultsRecorded: number; - errors?: string[]; -} - -export interface IImportRaceResultsApiPresenter extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/IImportRaceResultsPresenter.ts b/core/racing/application/presenters/IImportRaceResultsPresenter.ts deleted file mode 100644 index 1da1cf925..000000000 --- a/core/racing/application/presenters/IImportRaceResultsPresenter.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface ImportRaceResultsSummaryViewModel { - importedCount: number; - standingsRecalculated: boolean; -} - -export interface IImportRaceResultsPresenter { - present(viewModel: ImportRaceResultsSummaryViewModel): ImportRaceResultsSummaryViewModel; - getViewModel(): ImportRaceResultsSummaryViewModel | null; -} \ No newline at end of file diff --git a/core/racing/application/presenters/IJoinLeaguePresenter.ts b/core/racing/application/presenters/IJoinLeaguePresenter.ts deleted file mode 100644 index 582356ed0..000000000 --- a/core/racing/application/presenters/IJoinLeaguePresenter.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Presenter } from './Presenter'; - -export interface JoinLeagueResultDTO { - id: string; -} - -export interface JoinLeagueViewModel { - success: boolean; - membershipId?: string; - error?: string; -} - -export interface IJoinLeaguePresenter extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/ILeagueDriverSeasonStatsPresenter.ts b/core/racing/application/presenters/ILeagueDriverSeasonStatsPresenter.ts deleted file mode 100644 index 574c3b299..000000000 --- a/core/racing/application/presenters/ILeagueDriverSeasonStatsPresenter.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { Presenter } from '@core/shared/presentation'; - -export interface LeagueDriverSeasonStatsItemViewModel { - leagueId: string; - driverId: string; - position: number; - driverName: string; - teamId?: string; - teamName?: string; - totalPoints: number; - basePoints: number; - penaltyPoints: number; - bonusPoints: number; - pointsPerRace: number; - racesStarted: number; - racesFinished: number; - dnfs: number; - noShows: number; - avgFinish: number | null; - rating: number | null; - ratingChange: number | null; -} - -export interface LeagueDriverSeasonStatsViewModel { - leagueId: string; - stats: LeagueDriverSeasonStatsItemViewModel[]; -} - -export interface LeagueDriverSeasonStatsResultDTO { - leagueId: string; - standings: Array<{ - driverId: string; - position: number; - points: number; - racesCompleted: number; - }>; - penalties: Map; - driverResults: Map>; - driverRatings: Map; -} - -export interface ILeagueDriverSeasonStatsPresenter - extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/ILeagueFullConfigPresenter.ts b/core/racing/application/presenters/ILeagueFullConfigPresenter.ts deleted file mode 100644 index 5f57a2a00..000000000 --- a/core/racing/application/presenters/ILeagueFullConfigPresenter.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { League } from '../../domain/entities/League'; -import type { Season } from '../../domain/entities/Season'; -import type { LeagueScoringConfig } from '../../domain/entities/LeagueScoringConfig'; -import type { Game } from '../../domain/entities/Game'; -import type { Presenter } from '@core/shared/presentation'; - -export interface LeagueConfigFormViewModel { - leagueId: string; - basics: { - name: string; - description: string; - visibility: string; - gameId: string; - }; - structure: { - mode: string; - maxDrivers: number; - maxTeams?: number; - driversPerTeam?: number; - multiClassEnabled: boolean; - }; - championships: { - enableDriverChampionship: boolean; - enableTeamChampionship: boolean; - enableNationsChampionship: boolean; - enableTrophyChampionship: boolean; - }; - scoring: { - patternId?: string; - customScoringEnabled: boolean; - }; - dropPolicy: { - strategy: string; - n?: number; - }; - timings: { - practiceMinutes: number; - qualifyingMinutes: number; - sprintRaceMinutes?: number; - mainRaceMinutes: number; - sessionCount: number; - roundsPlanned: number; - }; - stewarding: { - decisionMode: string; - requireDefense: boolean; - defenseTimeLimit: number; - voteTimeLimit: number; - protestDeadlineHours: number; - stewardingClosesHours: number; - notifyAccusedOnProtest: boolean; - notifyOnVoteRequired: boolean; - requiredVotes?: number; - }; -} - -export interface LeagueFullConfigData { - league: League; - activeSeason?: Season; - scoringConfig?: LeagueScoringConfig; - game?: Game; -} - -export interface ILeagueFullConfigPresenter - extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/ILeagueSchedulePreviewPresenter.ts b/core/racing/application/presenters/ILeagueSchedulePreviewPresenter.ts deleted file mode 100644 index 82c011b20..000000000 --- a/core/racing/application/presenters/ILeagueSchedulePreviewPresenter.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { LeagueSchedulePreviewDTO } from '../dto/LeagueScheduleDTO'; - -export interface ILeagueSchedulePreviewPresenter { - present(data: LeagueSchedulePreviewDTO): void; -} \ No newline at end of file diff --git a/core/racing/application/presenters/ILeagueScoringConfigPresenter.ts b/core/racing/application/presenters/ILeagueScoringConfigPresenter.ts deleted file mode 100644 index 54f3bd7d2..000000000 --- a/core/racing/application/presenters/ILeagueScoringConfigPresenter.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { ChampionshipConfig } from '../../domain/types/ChampionshipConfig'; -import type { LeagueScoringPresetOutputPort } from '../ports/output/LeagueScoringPresetOutputPort'; -import type { Presenter } from '@core/shared/presentation'; - -export interface LeagueScoringChampionshipViewModel { - id: string; - name: string; - type: string; - sessionTypes: string[]; - pointsPreview: Array<{ sessionType: string; position: number; points: number }>; - bonusSummary: string[]; - dropPolicyDescription: string; -} - -export interface LeagueScoringConfigViewModel { - leagueId: string; - seasonId: string; - gameId: string; - gameName: string; - scoringPresetId?: string; - scoringPresetName?: string; - dropPolicySummary: string; - championships: LeagueScoringChampionshipViewModel[]; -} - -export interface LeagueScoringConfigData { - leagueId: string; - seasonId: string; - gameId: string; - gameName: string; - scoringPresetId?: string; - preset?: LeagueScoringPresetOutputPort; - championships: ChampionshipConfig[]; -} - -export interface ILeagueScoringConfigPresenter - extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/ILeagueScoringPresetsPresenter.ts b/core/racing/application/presenters/ILeagueScoringPresetsPresenter.ts deleted file mode 100644 index 8607bcfab..000000000 --- a/core/racing/application/presenters/ILeagueScoringPresetsPresenter.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { LeagueScoringPresetOutputPort } from '../ports/output/LeagueScoringPresetOutputPort'; -import type { Presenter } from '@core/shared/presentation'; - -export interface LeagueScoringPresetsViewModel { - presets: LeagueScoringPresetOutputPort[]; - totalCount: number; -} - -export interface LeagueScoringPresetsResultDTO { - presets: LeagueScoringPresetOutputPort[]; -} - -export interface ILeagueScoringPresetsPresenter - extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/ILeagueStandingsPresenter.ts b/core/racing/application/presenters/ILeagueStandingsPresenter.ts deleted file mode 100644 index 27db139e8..000000000 --- a/core/racing/application/presenters/ILeagueStandingsPresenter.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { Standing } from '../../domain/entities/Standing'; -import type { Presenter } from '@core/shared/presentation/Presenter'; - -export interface StandingItemViewModel { - driverId: string; - driver: { id: string; name: string }; - points: number; - rank: number; -} - -export interface LeagueStandingsViewModel { - standings: StandingItemViewModel[]; -} - -export interface LeagueStandingsResultDTO { - standings: Standing[]; - drivers: { id: string; name: string }[]; -} - -export interface ILeagueStandingsPresenter - extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/ILeagueStatsPresenter.ts b/core/racing/application/presenters/ILeagueStatsPresenter.ts deleted file mode 100644 index f499b720a..000000000 --- a/core/racing/application/presenters/ILeagueStatsPresenter.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Presenter } from '@core/shared/presentation/Presenter'; - -export interface LeagueStatsViewModel { - totalMembers: number; - totalRaces: number; - averageRating: number; -} - -export interface LeagueStatsResultDTO { - totalMembers: number; - totalRaces: number; - averageRating: number; -} - -export interface ILeagueStatsPresenter - extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/IPendingSponsorshipRequestsPresenter.ts b/core/racing/application/presenters/IPendingSponsorshipRequestsPresenter.ts deleted file mode 100644 index ddeea3d7c..000000000 --- a/core/racing/application/presenters/IPendingSponsorshipRequestsPresenter.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Presenter } from '@core/shared/presentation'; -import type { GetPendingSponsorshipRequestsResultDTO } from '../use-cases/GetPendingSponsorshipRequestsUseCase'; - -export type PendingSponsorshipRequestsViewModel = GetPendingSponsorshipRequestsResultDTO; - -export interface IPendingSponsorshipRequestsPresenter - extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/IProfileOverviewPresenter.ts b/core/racing/application/presenters/IProfileOverviewPresenter.ts deleted file mode 100644 index 515f03480..000000000 --- a/core/racing/application/presenters/IProfileOverviewPresenter.ts +++ /dev/null @@ -1,105 +0,0 @@ -export interface ProfileOverviewDriverSummaryViewModel { - id: string; - name: string; - country: string; - avatarUrl: string; - iracingId: string | null; - joinedAt: string; - rating: number | null; - globalRank: number | null; - consistency: number | null; - bio: string | null; - totalDrivers: number | null; -} - -export interface ProfileOverviewStatsViewModel { - totalRaces: number; - wins: number; - podiums: number; - dnfs: number; - avgFinish: number | null; - bestFinish: number | null; - worstFinish: number | null; - finishRate: number | null; - winRate: number | null; - podiumRate: number | null; - percentile: number | null; - rating: number | null; - consistency: number | null; - overallRank: number | null; -} - -export interface ProfileOverviewFinishDistributionViewModel { - totalRaces: number; - wins: number; - podiums: number; - topTen: number; - dnfs: number; - other: number; -} - -export interface ProfileOverviewTeamMembershipViewModel { - teamId: string; - teamName: string; - teamTag: string | null; - role: string; - joinedAt: string; - isCurrent: boolean; -} - -export interface ProfileOverviewSocialFriendSummaryViewModel { - id: string; - name: string; - country: string; - avatarUrl: string; -} - -export interface ProfileOverviewSocialSummaryViewModel { - friendsCount: number; - friends: ProfileOverviewSocialFriendSummaryViewModel[]; -} - -export type ProfileOverviewSocialPlatform = 'twitter' | 'youtube' | 'twitch' | 'discord'; - -export type ProfileOverviewAchievementRarity = 'common' | 'rare' | 'epic' | 'legendary'; - -export interface ProfileOverviewAchievementViewModel { - id: string; - title: string; - description: string; - icon: 'trophy' | 'medal' | 'star' | 'crown' | 'target' | 'zap'; - rarity: ProfileOverviewAchievementRarity; - earnedAt: string; -} - -export interface ProfileOverviewSocialHandleViewModel { - platform: ProfileOverviewSocialPlatform; - handle: string; - url: string; -} - -export interface ProfileOverviewExtendedProfileViewModel { - socialHandles: ProfileOverviewSocialHandleViewModel[]; - achievements: ProfileOverviewAchievementViewModel[]; - racingStyle: string; - favoriteTrack: string; - favoriteCar: string; - timezone: string; - availableHours: string; - lookingForTeam: boolean; - openToRequests: boolean; -} - -export interface ProfileOverviewViewModel { - currentDriver: ProfileOverviewDriverSummaryViewModel | null; - stats: ProfileOverviewStatsViewModel | null; - finishDistribution: ProfileOverviewFinishDistributionViewModel | null; - teamMemberships: ProfileOverviewTeamMembershipViewModel[]; - socialSummary: ProfileOverviewSocialSummaryViewModel; - extendedProfile: ProfileOverviewExtendedProfileViewModel | null; -} - -export interface IProfileOverviewPresenter { - present(viewModel: ProfileOverviewViewModel): void; - getViewModel(): ProfileOverviewViewModel | null; -} \ No newline at end of file diff --git a/core/racing/application/presenters/IRaceDetailPresenter.ts b/core/racing/application/presenters/IRaceDetailPresenter.ts deleted file mode 100644 index 664b5a20b..000000000 --- a/core/racing/application/presenters/IRaceDetailPresenter.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { SessionType, RaceStatus } from '../../domain/entities/Race'; -import type { Presenter } from '@core/shared/presentation'; - -export interface RaceDetailEntryViewModel { - id: string; - name: string; - country: string; - avatarUrl: string; - rating: number | null; - isCurrentUser: boolean; -} - -export interface RaceDetailUserResultViewModel { - position: number; - startPosition: number; - incidents: number; - fastestLap: number; - positionChange: number; - isPodium: boolean; - isClean: boolean; - ratingChange: number | null; -} - -export interface RaceDetailRaceViewModel { - id: string; - leagueId: string; - track: string; - car: string; - scheduledAt: string; - sessionType: SessionType; - status: RaceStatus; - strengthOfField: number | null; - registeredCount?: number; - maxParticipants?: number; -} - -export interface RaceDetailLeagueViewModel { - id: string; - name: string; - description: string; - settings: { - maxDrivers?: number; - qualifyingFormat?: string; - }; -} - -export interface RaceDetailViewModel { - race: RaceDetailRaceViewModel | null; - league: RaceDetailLeagueViewModel | null; - entryList: RaceDetailEntryViewModel[]; - registration: { - isUserRegistered: boolean; - canRegister: boolean; - }; - userResult: RaceDetailUserResultViewModel | null; - error?: string; -} - -export interface IRaceDetailPresenter - extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/IRacePenaltiesPresenter.ts b/core/racing/application/presenters/IRacePenaltiesPresenter.ts deleted file mode 100644 index f213f9bf9..000000000 --- a/core/racing/application/presenters/IRacePenaltiesPresenter.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { Penalty, PenaltyType, PenaltyStatus } from '../../domain/entities/Penalty'; -import type { Presenter } from '@core/shared/presentation/Presenter'; - -export interface RacePenaltyViewModel { - id: string; - raceId: string; - driverId: string; - driverName: string; - type: PenaltyType; - value?: number; - reason: string; - protestId?: string; - issuedBy: string; - issuedByName: string; - status: PenaltyStatus; - description: string; - issuedAt: string; - appliedAt?: string; - notes?: string; -} - -export interface RacePenaltiesViewModel { - penalties: RacePenaltyViewModel[]; -} - -export interface RacePenaltiesResultDTO { - penalties: Penalty[]; - driverMap: Map; -} - -export interface IRacePenaltiesPresenter - extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/IRaceProtestsPresenter.ts b/core/racing/application/presenters/IRaceProtestsPresenter.ts deleted file mode 100644 index 2f7ae37b1..000000000 --- a/core/racing/application/presenters/IRaceProtestsPresenter.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { Protest, ProtestStatus, ProtestIncident } from '../../domain/entities/Protest'; -import type { Presenter } from '@core/shared/presentation/Presenter'; - -export interface RaceProtestViewModel { - id: string; - raceId: string; - protestingDriverId: string; - protestingDriverName: string; - accusedDriverId: string; - accusedDriverName: string; - incident: ProtestIncident; - comment?: string; - proofVideoUrl?: string; - status: ProtestStatus; - reviewedBy?: string; - reviewedByName?: string; - decisionNotes?: string; - filedAt: string; - reviewedAt?: string; -} - -export interface RaceProtestsViewModel { - protests: RaceProtestViewModel[]; -} - -export interface RaceProtestsResultDTO { - protests: Protest[]; - driverMap: Map; -} - -export interface IRaceProtestsPresenter - extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/IRaceRegistrationsPresenter.ts b/core/racing/application/presenters/IRaceRegistrationsPresenter.ts deleted file mode 100644 index 4e421d333..000000000 --- a/core/racing/application/presenters/IRaceRegistrationsPresenter.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { Presenter } from '@core/shared/presentation'; - -export interface RaceRegistrationsViewModel { - registeredDriverIds: string[]; - count: number; -} - -export interface RaceRegistrationsResultDTO { - registeredDriverIds: string[]; -} - -export interface IRaceRegistrationsPresenter - extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/IRaceResultsDetailPresenter.ts b/core/racing/application/presenters/IRaceResultsDetailPresenter.ts deleted file mode 100644 index 07601023d..000000000 --- a/core/racing/application/presenters/IRaceResultsDetailPresenter.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { RaceStatus } from '../../domain/entities/Race'; -import type { Result } from '../../domain/entities/Result'; -import type { Driver } from '../../domain/entities/Driver'; -import type { PenaltyType } from '../../domain/entities/Penalty'; -import type { Presenter } from '@core/shared/presentation'; - -export interface RaceResultsHeaderViewModel { - id: string; - leagueId: string; - track: string; - scheduledAt: Date; - status: RaceStatus; -} - -export interface RaceResultsLeagueViewModel { - id: string; - name: string; -} - -export interface RaceResultsPenaltySummaryViewModel { - driverId: string; - type: PenaltyType; - value?: number; -} - -export interface RaceResultsDetailViewModel { - race: RaceResultsHeaderViewModel | null; - league: RaceResultsLeagueViewModel | null; - results: Result[]; - drivers: Driver[]; - penalties: RaceResultsPenaltySummaryViewModel[]; - pointsSystem?: Record; - fastestLapTime?: number; - currentDriverId?: string; - error?: string; -} - -export interface IRaceResultsDetailPresenter - extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/IRaceWithSOFPresenter.ts b/core/racing/application/presenters/IRaceWithSOFPresenter.ts deleted file mode 100644 index 7cc561bee..000000000 --- a/core/racing/application/presenters/IRaceWithSOFPresenter.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { Presenter } from '@core/shared/presentation'; - -export interface RaceWithSOFViewModel { - id: string; - leagueId: string; - scheduledAt: string; - track: string; - trackId: string; - car: string; - carId: string; - sessionType: string; - status: string; - strengthOfField: number | null; - registeredCount: number; - maxParticipants: number; - participantCount: number; -} - -export interface RaceWithSOFResultDTO { - raceId: string; - leagueId: string; - scheduledAt: Date; - track: string; - trackId: string; - car: string; - carId: string; - sessionType: string; - status: string; - strengthOfField: number | null; - registeredCount: number; - maxParticipants: number; - participantCount: number; -} - -export interface IRaceWithSOFPresenter - extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/IRacesPagePresenter.ts b/core/racing/application/presenters/IRacesPagePresenter.ts deleted file mode 100644 index d65c671e9..000000000 --- a/core/racing/application/presenters/IRacesPagePresenter.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { Presenter } from '@core/shared/presentation'; - -export interface RaceListItemViewModel { - id: string; - track: string; - car: string; - scheduledAt: string; - status: 'scheduled' | 'running' | 'completed' | 'cancelled'; - leagueId: string; - leagueName: string; - strengthOfField: number | null; - isUpcoming: boolean; - isLive: boolean; - isPast: boolean; -} - -export interface RacesPageViewModel { - races: RaceListItemViewModel[]; - stats: { - total: number; - scheduled: number; - running: number; - completed: number; - }; - liveRaces: RaceListItemViewModel[]; - upcomingThisWeek: RaceListItemViewModel[]; - recentResults: RaceListItemViewModel[]; -} - -export interface RacesPageResultDTO { - races: unknown[]; -} - -export interface IRacesPagePresenter - extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/IRejectLeagueJoinRequestPresenter.ts b/core/racing/application/presenters/IRejectLeagueJoinRequestPresenter.ts deleted file mode 100644 index d228a101a..000000000 --- a/core/racing/application/presenters/IRejectLeagueJoinRequestPresenter.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { Presenter } from '@core/shared/presentation/Presenter'; - -export interface RejectLeagueJoinRequestViewModel { - success: boolean; - message: string; -} - -export interface RejectLeagueJoinRequestResultDTO { - success: boolean; - message: string; -} - -export interface IRejectLeagueJoinRequestPresenter extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/IRemoveLeagueMemberPresenter.ts b/core/racing/application/presenters/IRemoveLeagueMemberPresenter.ts deleted file mode 100644 index 503a03d68..000000000 --- a/core/racing/application/presenters/IRemoveLeagueMemberPresenter.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Presenter } from './Presenter'; - -export interface RemoveLeagueMemberResultDTO { - success: boolean; -} - -export interface RemoveLeagueMemberViewModel { - success: boolean; -} - -export interface IRemoveLeagueMemberPresenter extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/ISponsorDashboardPresenter.ts b/core/racing/application/presenters/ISponsorDashboardPresenter.ts deleted file mode 100644 index 25db8ca2b..000000000 --- a/core/racing/application/presenters/ISponsorDashboardPresenter.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { SponsorDashboardDTO } from '../use-cases/GetSponsorDashboardUseCase'; -import type { Presenter } from '@core/shared/presentation'; - -export type SponsorDashboardViewModel = SponsorDashboardDTO | null; - -export interface ISponsorDashboardPresenter - extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/ISponsorSponsorshipsPresenter.ts b/core/racing/application/presenters/ISponsorSponsorshipsPresenter.ts deleted file mode 100644 index aba5d6773..000000000 --- a/core/racing/application/presenters/ISponsorSponsorshipsPresenter.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { SponsorSponsorshipsDTO } from '../use-cases/GetSponsorSponsorshipsUseCase'; -import type { Presenter } from '@core/shared/presentation'; - -export type SponsorSponsorshipsViewModel = SponsorSponsorshipsDTO | null; - -export interface ISponsorSponsorshipsPresenter - extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/ITeamDetailsPresenter.ts b/core/racing/application/presenters/ITeamDetailsPresenter.ts deleted file mode 100644 index ce869c0d8..000000000 --- a/core/racing/application/presenters/ITeamDetailsPresenter.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { Team } from '../../domain/entities/Team'; -import type { TeamMembership } from '../../domain/types/TeamMembership'; -import type { Presenter } from '@core/shared/presentation'; - -export interface TeamDetailsViewModel { - team: { - id: string; - name: string; - tag: string; - description: string; - ownerId: string; - leagues: string[]; - createdAt: string; - }; - membership: { - role: 'owner' | 'manager' | 'member'; - joinedAt: string; - isActive: boolean; - } | null; - canManage: boolean; -} - -export interface TeamDetailsResultDTO { - team: Team; - membership: TeamMembership | null; - driverId: string; -} - -export interface ITeamDetailsPresenter - extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/ITeamJoinRequestsPresenter.ts b/core/racing/application/presenters/ITeamJoinRequestsPresenter.ts deleted file mode 100644 index a1ed0ffb1..000000000 --- a/core/racing/application/presenters/ITeamJoinRequestsPresenter.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { TeamJoinRequest } from '../../domain/types/TeamMembership'; -import type { Presenter } from '@core/shared/presentation'; - -export interface TeamJoinRequestViewModel { - requestId: string; - driverId: string; - driverName: string; - teamId: string; - status: 'pending' | 'approved' | 'rejected'; - requestedAt: string; - avatarUrl: string; -} - -export interface TeamJoinRequestsViewModel { - requests: TeamJoinRequestViewModel[]; - pendingCount: number; - totalCount: number; -} - -export interface TeamJoinRequestsResultDTO { - requests: TeamJoinRequest[]; - driverNames: Record; - avatarUrls: Record; -} - -export interface ITeamJoinRequestsPresenter - extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/ITeamMembersPresenter.ts b/core/racing/application/presenters/ITeamMembersPresenter.ts deleted file mode 100644 index a717de5c5..000000000 --- a/core/racing/application/presenters/ITeamMembersPresenter.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { TeamMembership } from '../../domain/types/TeamMembership'; -import type { Presenter } from '@core/shared/presentation'; - -export interface TeamMemberViewModel { - driverId: string; - driverName: string; - role: 'owner' | 'manager' | 'member'; - joinedAt: string; - isActive: boolean; - avatarUrl: string; -} - -export interface TeamMembersViewModel { - members: TeamMemberViewModel[]; - totalCount: number; - ownerCount: number; - managerCount: number; - memberCount: number; -} - -export interface TeamMembersResultDTO { - memberships: TeamMembership[]; - driverNames: Record; - avatarUrls: Record; -} - -export interface ITeamMembersPresenter - extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/ITeamsLeaderboardPresenter.ts b/core/racing/application/presenters/ITeamsLeaderboardPresenter.ts deleted file mode 100644 index c38106c72..000000000 --- a/core/racing/application/presenters/ITeamsLeaderboardPresenter.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { Presenter } from '@core/shared/presentation'; - -export type SkillLevel = 'beginner' | 'intermediate' | 'advanced' | 'pro'; - -export interface TeamLeaderboardItemViewModel { - id: string; - name: string; - memberCount: number; - rating: number | null; - totalWins: number; - totalRaces: number; - performanceLevel: SkillLevel; - isRecruiting: boolean; - createdAt: Date; - description?: string; - specialization?: 'endurance' | 'sprint' | 'mixed'; - region?: string; - languages?: string[]; -} - -export interface TeamsLeaderboardViewModel { - teams: TeamLeaderboardItemViewModel[]; - recruitingCount: number; - /** - * Teams grouped by their skill level for UI display. - */ - groupsBySkillLevel: Record; - /** - * Precomputed top teams ordered for leaderboard preview. - */ - topTeams: TeamLeaderboardItemViewModel[]; -} - -export interface TeamsLeaderboardResultDTO { - teams: unknown[]; - recruitingCount: number; -} - -export interface ITeamsLeaderboardPresenter - extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/ITotalDriversPresenter.ts b/core/racing/application/presenters/ITotalDriversPresenter.ts deleted file mode 100644 index 5263de7a9..000000000 --- a/core/racing/application/presenters/ITotalDriversPresenter.ts +++ /dev/null @@ -1,13 +0,0 @@ -export interface TotalDriversViewModel { - totalDrivers: number; -} - -export interface TotalDriversResultDTO { - totalDrivers: number; -} - -export interface ITotalDriversPresenter { - present(dto: TotalDriversResultDTO): void; - get viewModel(): TotalDriversViewModel; - reset(): void; -} \ No newline at end of file diff --git a/core/racing/application/presenters/ITransferLeagueOwnershipPresenter.ts b/core/racing/application/presenters/ITransferLeagueOwnershipPresenter.ts deleted file mode 100644 index 2c0fc103a..000000000 --- a/core/racing/application/presenters/ITransferLeagueOwnershipPresenter.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Presenter } from './Presenter'; - -export interface TransferLeagueOwnershipResultDTO { - success: boolean; -} - -export interface TransferLeagueOwnershipViewModel { - success: boolean; - error?: string; -} - -export interface ITransferLeagueOwnershipPresenter extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/presenters/IUpdateLeagueMemberRolePresenter.ts b/core/racing/application/presenters/IUpdateLeagueMemberRolePresenter.ts deleted file mode 100644 index e100ff867..000000000 --- a/core/racing/application/presenters/IUpdateLeagueMemberRolePresenter.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Presenter } from './Presenter'; - -export interface UpdateLeagueMemberRoleResultDTO { - success: boolean; -} - -export interface UpdateLeagueMemberRoleViewModel { - success: boolean; -} - -export interface IUpdateLeagueMemberRolePresenter extends Presenter {} \ No newline at end of file diff --git a/core/racing/application/use-cases/CompleteDriverOnboardingUseCase.test.ts b/core/racing/application/use-cases/CompleteDriverOnboardingUseCase.test.ts index 73e0bf46a..70ad97ce8 100644 --- a/core/racing/application/use-cases/CompleteDriverOnboardingUseCase.test.ts +++ b/core/racing/application/use-cases/CompleteDriverOnboardingUseCase.test.ts @@ -49,10 +49,10 @@ describe('CompleteDriverOnboardingUseCase', () => { expect(driverRepository.create).toHaveBeenCalledWith( expect.objectContaining({ id: 'user-1', - iracingId: 'user-1', - name: 'John Doe', - country: 'US', - bio: 'Test bio', + iracingId: expect.objectContaining({ value: 'user-1' }), + name: expect.objectContaining({ value: 'John Doe' }), + country: expect.objectContaining({ value: 'US' }), + bio: expect.objectContaining({ value: 'Test bio' }), }) ); }); @@ -123,9 +123,9 @@ describe('CompleteDriverOnboardingUseCase', () => { expect(driverRepository.create).toHaveBeenCalledWith( expect.objectContaining({ id: 'user-1', - iracingId: 'user-1', - name: 'John Doe', - country: 'US', + iracingId: expect.objectContaining({ value: 'user-1' }), + name: expect.objectContaining({ value: 'John Doe' }), + country: expect.objectContaining({ value: 'US' }), bio: undefined, }) ); diff --git a/core/racing/application/use-cases/CompleteDriverOnboardingUseCase.ts b/core/racing/application/use-cases/CompleteDriverOnboardingUseCase.ts index a16a9d157..58d4546d3 100644 --- a/core/racing/application/use-cases/CompleteDriverOnboardingUseCase.ts +++ b/core/racing/application/use-cases/CompleteDriverOnboardingUseCase.ts @@ -4,16 +4,17 @@ import { Driver } from '../../domain/entities/Driver'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { CompleteDriverOnboardingCommand } from '../dto/CompleteDriverOnboardingCommand'; +import type { CompleteDriverOnboardingOutputPort } from '../ports/output/CompleteDriverOnboardingOutputPort'; /** * Use Case for completing driver onboarding. */ export class CompleteDriverOnboardingUseCase - implements AsyncUseCase + implements AsyncUseCase { constructor(private readonly driverRepository: IDriverRepository) {} - async execute(command: CompleteDriverOnboardingCommand): Promise>> { + async execute(command: CompleteDriverOnboardingCommand): Promise>> { try { // Check if driver already exists const existing = await this.driverRepository.findById(command.userId); diff --git a/core/racing/application/use-cases/DashboardOverviewUseCase.ts b/core/racing/application/use-cases/DashboardOverviewUseCase.ts index f3ce949b7..2167a01b3 100644 --- a/core/racing/application/use-cases/DashboardOverviewUseCase.ts +++ b/core/racing/application/use-cases/DashboardOverviewUseCase.ts @@ -17,15 +17,15 @@ import { Driver } from '../../domain/entities/Driver'; import { Standing } from '../../domain/entities/Standing'; import type { FeedItem } from '@core/social/domain/types/FeedItem'; import type { - DashboardOverviewViewModel, - DashboardDriverSummaryViewModel, - DashboardRaceSummaryViewModel, - DashboardRecentResultViewModel, - DashboardLeagueStandingSummaryViewModel, - DashboardFeedItemSummaryViewModel, - DashboardFeedSummaryViewModel, - DashboardFriendSummaryViewModel, -} from '../presenters/IDashboardOverviewPresenter'; + DashboardOverviewOutputPort, + DashboardDriverSummaryOutputPort, + DashboardRaceSummaryOutputPort, + DashboardRecentResultOutputPort, + DashboardLeagueStandingSummaryOutputPort, + DashboardFeedItemSummaryOutputPort, + DashboardFeedSummaryOutputPort, + DashboardFriendSummaryOutputPort, +} from '../ports/output/DashboardOverviewOutputPort'; interface DashboardOverviewParams { driverId: string; @@ -55,7 +55,7 @@ export class DashboardOverviewUseCase { private readonly getDriverStats: (driverId: string) => DashboardDriverStatsAdapter | null, ) {} - async execute(params: DashboardOverviewParams): Promise> { + async execute(params: DashboardOverviewParams): Promise> { const { driverId } = params; const [driver, allLeagues, allRaces, allResults, feedItems, friends] = await Promise.all([ @@ -71,7 +71,7 @@ export class DashboardOverviewUseCase { const driverStats = this.getDriverStats(driverId); - const currentDriver: DashboardDriverSummaryViewModel | null = driver + const currentDriver: DashboardDriverSummaryOutputPort | null = driver ? { id: driver.id, name: driver.name, @@ -101,10 +101,10 @@ export class DashboardOverviewUseCase { const { myUpcomingRaces, otherUpcomingRaces } = await this.partitionUpcomingRacesByRegistration(upcomingRacesInDriverLeagues, driverId, leagueMap); - const nextRace: DashboardRaceSummaryViewModel | null = + const nextRace: DashboardRaceSummaryOutputPort | null = myUpcomingRaces.length > 0 ? myUpcomingRaces[0]! : null; - const upcomingRacesSummaries: DashboardRaceSummaryViewModel[] = [ + const upcomingRacesSummaries: DashboardRaceSummaryOutputPort[] = [ ...myUpcomingRaces, ...otherUpcomingRaces, ].slice().sort( @@ -128,7 +128,7 @@ export class DashboardOverviewUseCase { const friendsSummary = await this.buildFriendsSummary(friends); - const viewModel: DashboardOverviewViewModel = { + const viewModel: DashboardOverviewOutputPort = { currentDriver, myUpcomingRaces, otherUpcomingRaces, @@ -162,11 +162,11 @@ export class DashboardOverviewUseCase { driverId: string, leagueMap: Map, ): Promise<{ - myUpcomingRaces: DashboardRaceSummaryViewModel[]; - otherUpcomingRaces: DashboardRaceSummaryViewModel[]; + myUpcomingRaces: DashboardRaceSummaryOutputPort[]; + otherUpcomingRaces: DashboardRaceSummaryOutputPort[]; }> { - const myUpcomingRaces: DashboardRaceSummaryViewModel[] = []; - const otherUpcomingRaces: DashboardRaceSummaryViewModel[] = []; + const myUpcomingRaces: DashboardRaceSummaryOutputPort[] = []; + const otherUpcomingRaces: DashboardRaceSummaryOutputPort[] = []; for (const race of upcomingRaces) { const isRegistered = await this.raceRegistrationRepository.isRegistered(race.id, driverId); @@ -186,7 +186,7 @@ export class DashboardOverviewUseCase { race: Race, leagueMap: Map, isMyLeague: boolean, - ): DashboardRaceSummaryViewModel { + ): DashboardRaceSummaryOutputPort { return { id: race.id, leagueId: race.leagueId, @@ -204,7 +204,7 @@ export class DashboardOverviewUseCase { allRaces: Race[], allLeagues: League[], driverId: string, - ): DashboardRecentResultViewModel[] { + ): DashboardRecentResultOutputPort[] { const raceById = new Map(allRaces.map(race => [race.id, race])); const leagueById = new Map(allLeagues.map(league => [league.id, league])); @@ -219,7 +219,7 @@ export class DashboardOverviewUseCase { const finishedAt = race.scheduledAt.toISOString(); - const item: DashboardRecentResultViewModel = { + const item: DashboardRecentResultOutputPort = { raceId: race.id, raceName: race.track, leagueId: race.leagueId, @@ -231,7 +231,7 @@ export class DashboardOverviewUseCase { return item; }) - .filter((item): item is DashboardRecentResultViewModel => !!item) + .filter((item): item is DashboardRecentResultOutputPort => !!item) .sort( (a, b) => new Date(b.finishedAt).getTime() - new Date(a.finishedAt).getTime(), @@ -245,8 +245,8 @@ export class DashboardOverviewUseCase { private async buildLeagueStandingsSummaries( driverLeagues: League[], driverId: string, - ): Promise { - const summaries: DashboardLeagueStandingSummaryViewModel[] = []; + ): Promise { + const summaries: DashboardLeagueStandingSummaryOutputPort[] = []; for (const league of driverLeagues.slice(0, 3)) { const standings = await this.standingRepository.findByLeagueId(league.id); @@ -267,8 +267,8 @@ export class DashboardOverviewUseCase { } private computeActiveLeaguesCount( - upcomingRaces: DashboardRaceSummaryViewModel[], - leagueStandingsSummaries: DashboardLeagueStandingSummaryViewModel[], + upcomingRaces: DashboardRaceSummaryOutputPort[], + leagueStandingsSummaries: DashboardLeagueStandingSummaryOutputPort[], ): number { const activeLeagueIds = new Set(); @@ -283,8 +283,8 @@ export class DashboardOverviewUseCase { return activeLeagueIds.size; } - private buildFeedSummary(feedItems: FeedItem[]): DashboardFeedSummaryViewModel { - const items: DashboardFeedItemSummaryViewModel[] = feedItems.map(item => ({ + private buildFeedSummary(feedItems: FeedItem[]): DashboardFeedSummaryOutputPort { + const items: DashboardFeedItemSummaryOutputPort[] = feedItems.map(item => ({ id: item.id, type: item.type, headline: item.headline, @@ -303,8 +303,8 @@ export class DashboardOverviewUseCase { }; } - private async buildFriendsSummary(friends: Driver[]): Promise { - const friendSummaries: DashboardFriendSummaryViewModel[] = []; + private async buildFriendsSummary(friends: Driver[]): Promise { + const friendSummaries: DashboardFriendSummaryOutputPort[] = []; for (const friend of friends) { const avatarResult = await this.getDriverAvatar({ driverId: friend.id }); diff --git a/core/racing/application/use-cases/GetAllLeaguesWithCapacityAndScoringUseCase.test.ts b/core/racing/application/use-cases/GetAllLeaguesWithCapacityAndScoringUseCase.test.ts index aa8740931..be6a2a3f8 100644 --- a/core/racing/application/use-cases/GetAllLeaguesWithCapacityAndScoringUseCase.test.ts +++ b/core/racing/application/use-cases/GetAllLeaguesWithCapacityAndScoringUseCase.test.ts @@ -54,15 +54,17 @@ describe('GetAllLeaguesWithCapacityAndScoringUseCase', () => { const result = await useCase.execute(); expect(result.isOk()).toBe(true); - expect(result.value).toEqual([ - { - league, - usedDriverSlots: 2, - season, - scoringConfig, - game, - preset, - }, - ]); + expect(result.value).toEqual({ + leagues: [ + { + league, + usedDriverSlots: 2, + season, + scoringConfig, + game, + preset, + }, + ], + }); }); }); diff --git a/core/racing/application/use-cases/GetAllLeaguesWithCapacityAndScoringUseCase.ts b/core/racing/application/use-cases/GetAllLeaguesWithCapacityAndScoringUseCase.ts index d95f4d28c..e6c573148 100644 --- a/core/racing/application/use-cases/GetAllLeaguesWithCapacityAndScoringUseCase.ts +++ b/core/racing/application/use-cases/GetAllLeaguesWithCapacityAndScoringUseCase.ts @@ -4,7 +4,7 @@ import type { ISeasonRepository } from '../../domain/repositories/ISeasonReposit import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository'; import type { IGameRepository } from '../../domain/repositories/IGameRepository'; import type { LeagueScoringPresetProvider } from '../ports/LeagueScoringPresetProvider'; -import type { LeagueEnrichedData } from '../presenters/IAllLeaguesWithCapacityAndScoringPresenter'; +import type { LeagueEnrichedData, AllLeaguesWithCapacityAndScoringOutputPort } from '../ports/output/AllLeaguesWithCapacityAndScoringOutputPort'; import { Result } from '@core/shared/application/Result'; /** @@ -22,7 +22,7 @@ export class GetAllLeaguesWithCapacityAndScoringUseCase private readonly presetProvider: LeagueScoringPresetProvider, ) {} - async execute(): Promise> { + async execute(): Promise> { const leagues = await this.leagueRepository.findAll(); const enrichedLeagues: LeagueEnrichedData[] = []; @@ -72,7 +72,7 @@ export class GetAllLeaguesWithCapacityAndScoringUseCase ...(preset ? { preset } : {}), }); } -return Result.ok(enrichedLeagues); + return Result.ok({ leagues: enrichedLeagues }); } } \ No newline at end of file diff --git a/core/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase.ts b/core/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase.ts index f186507a8..e7ff30a28 100644 --- a/core/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase.ts +++ b/core/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase.ts @@ -1,6 +1,6 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository'; import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository'; -import type { AllLeaguesWithCapacityResultDTO } from '../presenters/IAllLeaguesWithCapacityPresenter'; +import type { AllLeaguesWithCapacityOutputPort } from '../ports/output/AllLeaguesWithCapacityOutputPort'; import type { AsyncUseCase } from '@core/shared/application'; import { Result } from '@/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; @@ -10,17 +10,17 @@ import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorC * Orchestrates domain logic and returns result. */ export class GetAllLeaguesWithCapacityUseCase - implements AsyncUseCase + implements AsyncUseCase { constructor( private readonly leagueRepository: ILeagueRepository, private readonly leagueMembershipRepository: ILeagueMembershipRepository, ) {} - async execute(): Promise>> { + async execute(): Promise>> { const leagues = await this.leagueRepository.findAll(); - const memberCounts = new Map(); + const memberCounts: Record = {}; for (const league of leagues) { const members = await this.leagueMembershipRepository.getLeagueMembers(league.id); @@ -34,14 +34,14 @@ export class GetAllLeaguesWithCapacityUseCase m.role === 'member'), ).length; - memberCounts.set(league.id, usedSlots); + memberCounts[league.id] = usedSlots; } - const dto: AllLeaguesWithCapacityResultDTO = { + const output: AllLeaguesWithCapacityOutputPort = { leagues, memberCounts, }; - return Result.ok(dto); + return Result.ok(output); } } \ No newline at end of file diff --git a/core/racing/application/use-cases/GetAllRacesPageDataUseCase.ts b/core/racing/application/use-cases/GetAllRacesPageDataUseCase.ts index d5abd589c..3c16019dd 100644 --- a/core/racing/application/use-cases/GetAllRacesPageDataUseCase.ts +++ b/core/racing/application/use-cases/GetAllRacesPageDataUseCase.ts @@ -1,18 +1,12 @@ import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository'; -import type { Logger } from '@core/shared/application'; -import type { - AllRacesPageResultDTO, - AllRacesPageViewModel, - AllRacesListItemViewModel, - AllRacesFilterOptionsViewModel, -} from '../presenters/IAllRacesPagePresenter'; -import type { AsyncUseCase } from '@core/shared/application'; +import type { Logger , AsyncUseCase } from '@core/shared/application'; +import type { AllRacesPageOutputPort, AllRacesListItem, AllRacesFilterOptions } from '../ports/output/AllRacesPageOutputPort'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; export class GetAllRacesPageDataUseCase - implements AsyncUseCase { + implements AsyncUseCase { constructor( private readonly raceRepository: IRaceRepository, private readonly leagueRepository: ILeagueRepository, @@ -30,7 +24,7 @@ export class GetAllRacesPageDataUseCase const leagueMap = new Map(allLeagues.map((league) => [league.id.toString(), league.name.toString()])); - const races: AllRacesListItemViewModel[] = allRaces + const races: AllRacesListItem[] = allRaces .slice() .sort((a, b) => b.scheduledAt.getTime() - a.scheduledAt.getTime()) .map((race) => ({ @@ -49,7 +43,7 @@ export class GetAllRacesPageDataUseCase uniqueLeagues.set(league.id.toString(), { id: league.id.toString(), name: league.name.toString() }); } - const filters: AllRacesFilterOptionsViewModel = { + const filters: AllRacesFilterOptions = { statuses: [ { value: 'all', label: 'All Statuses' }, { value: 'scheduled', label: 'Scheduled' }, @@ -60,7 +54,7 @@ export class GetAllRacesPageDataUseCase leagues: Array.from(uniqueLeagues.values()), }; - const viewModel: AllRacesPageViewModel = { + const viewModel: AllRacesPageOutputPort = { races, filters, }; diff --git a/core/racing/application/use-cases/GetAllRacesUseCase.ts b/core/racing/application/use-cases/GetAllRacesUseCase.ts index 75790071e..a353471cd 100644 --- a/core/racing/application/use-cases/GetAllRacesUseCase.ts +++ b/core/racing/application/use-cases/GetAllRacesUseCase.ts @@ -1,38 +1,40 @@ import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository'; -import type { GetAllRacesResultDTO } from '../presenters/IGetAllRacesPresenter'; +import type { GetAllRacesOutputPort } from '../ports/output/GetAllRacesOutputPort'; import type { AsyncUseCase, Logger } from '@core/shared/application'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; -export class GetAllRacesUseCase implements AsyncUseCase { +export class GetAllRacesUseCase implements AsyncUseCase { constructor( private readonly raceRepository: IRaceRepository, private readonly leagueRepository: ILeagueRepository, private readonly logger: Logger, ) {} - async execute(): Promise>> { + async execute(): Promise>> { this.logger.debug('Executing GetAllRacesUseCase'); try { const races = await this.raceRepository.findAll(); const leagues = await this.leagueRepository.findAll(); const leagueMap = new Map(leagues.map(league => [league.id, league.name])); - const raceViewModels = races.map(race => ({ - id: race.id, - name: `${race.track} - ${race.car}`, - date: race.scheduledAt.toISOString(), - leagueName: leagueMap.get(race.leagueId) || 'Unknown League', - })); - - const dto: GetAllRacesResultDTO = { - races: raceViewModels, + const output: GetAllRacesOutputPort = { + races: races.map(race => ({ + id: race.id, + leagueId: race.leagueId, + track: race.track, + car: race.car, + status: race.status as 'scheduled' | 'running' | 'completed' | 'cancelled', + scheduledAt: race.scheduledAt.toISOString(), + strengthOfField: race.strengthOfField || null, + leagueName: (leagueMap.get(race.leagueId) || 'Unknown League').toString(), + })), totalCount: races.length, }; this.logger.debug('Successfully retrieved all races.'); - return Result.ok(dto); + return Result.ok(output); } catch (error) { this.logger.error('Error executing GetAllRacesUseCase', error instanceof Error ? error : new Error(String(error))); return Result.err({ diff --git a/core/racing/application/use-cases/GetAllTeamsUseCase.ts b/core/racing/application/use-cases/GetAllTeamsUseCase.ts index f3a796647..cc723ee49 100644 --- a/core/racing/application/use-cases/GetAllTeamsUseCase.ts +++ b/core/racing/application/use-cases/GetAllTeamsUseCase.ts @@ -1,6 +1,6 @@ import type { ITeamRepository } from '../../domain/repositories/ITeamRepository'; import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository'; -import type { AllTeamsResultDTO } from '../presenters/IAllTeamsPresenter'; +import type { GetAllTeamsOutputPort } from '../ports/output/GetAllTeamsOutputPort'; import type { AsyncUseCase, Logger } from '@core/shared/application'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; @@ -8,7 +8,7 @@ import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorC /** * Use Case for retrieving all teams. */ -export class GetAllTeamsUseCase implements AsyncUseCase { +export class GetAllTeamsUseCase implements AsyncUseCase { constructor( private readonly teamRepository: ITeamRepository, private readonly teamMembershipRepository: ITeamMembershipRepository, @@ -37,8 +37,9 @@ export class GetAllTeamsUseCase implements AsyncUseCase>> + implements AsyncUseCase<{ driverId: string }, Result>> { constructor( private readonly teamRepository: ITeamRepository, @@ -18,7 +18,7 @@ export class GetDriverTeamUseCase private readonly logger: Logger, ) {} - async execute(input: { driverId: string }): Promise>> { + async execute(input: { driverId: string }): Promise>> { this.logger.debug(`Executing GetDriverTeamUseCase for driverId: ${input.driverId}`); try { const membership = await this.membershipRepository.getActiveMembershipForDriver(input.driverId); @@ -35,14 +35,22 @@ export class GetDriverTeamUseCase } this.logger.debug(`Found team for teamId: ${team.id}, name: ${team.name}`); - const dto: DriverTeamResultDTO = { - team, - membership, + const output: DriverTeamOutputPort = { driverId: input.driverId, + team: { + id: team.id, + name: team.name.value, + tag: team.tag.value, + description: team.description.value, + ownerId: team.ownerId.value, + leagues: team.leagues.map(l => l.value), + createdAt: team.createdAt.value, + }, + membership, }; this.logger.info(`Successfully retrieved driver team for driverId: ${input.driverId}`); - return Result.ok(dto); + return Result.ok(output); } catch (error) { this.logger.error('Error executing GetDriverTeamUseCase', error instanceof Error ? error : new Error(String(error))); return Result.err({ code: 'REPOSITORY_ERROR', details: { message: error instanceof Error ? error.message : 'Unknown error occurred' } }); diff --git a/core/racing/application/use-cases/GetDriversLeaderboardUseCase.test.ts b/core/racing/application/use-cases/GetDriversLeaderboardUseCase.test.ts index 18682c4f5..316fec8d1 100644 --- a/core/racing/application/use-cases/GetDriversLeaderboardUseCase.test.ts +++ b/core/racing/application/use-cases/GetDriversLeaderboardUseCase.test.ts @@ -47,11 +47,14 @@ describe('GetDriversLeaderboardUseCase', () => { mockLogger, ); - const driver1 = { id: 'driver1', name: 'Driver One' }; - const driver2 = { id: 'driver2', name: 'Driver Two' }; - const rankings = { driver1: 1, driver2: 2 }; - const stats1 = { wins: 5, losses: 2 }; - const stats2 = { wins: 3, losses: 1 }; + const driver1 = { id: 'driver1', name: { value: 'Driver One' }, country: { value: 'US' } }; + const driver2 = { id: 'driver2', name: { value: 'Driver Two' }, country: { value: 'US' } }; + const rankings = [ + { driverId: 'driver1', rating: 2500, overallRank: 1 }, + { driverId: 'driver2', rating: 2400, overallRank: 2 }, + ]; + const stats1 = { totalRaces: 10, wins: 5, podiums: 7 }; + const stats2 = { totalRaces: 8, wins: 3, podiums: 4 }; mockDriverFindAll.mockResolvedValue([driver1, driver2]); mockRankingGetAllDriverRankings.mockReturnValue(rankings); @@ -70,10 +73,37 @@ describe('GetDriversLeaderboardUseCase', () => { expect(result.isOk()).toBe(true); expect(result.value).toEqual({ - drivers: [driver1, driver2], - rankings, - stats: { driver1: stats1, driver2: stats2 }, - avatarUrls: { driver1: 'avatar-driver1', driver2: 'avatar-driver2' }, + drivers: [ + { + id: 'driver1', + name: 'Driver One', + rating: 2500, + skillLevel: 'Pro', + nationality: 'US', + racesCompleted: 10, + wins: 5, + podiums: 7, + isActive: true, + rank: 1, + avatarUrl: 'avatar-driver1', + }, + { + id: 'driver2', + name: 'Driver Two', + rating: 2400, + skillLevel: 'Pro', + nationality: 'US', + racesCompleted: 8, + wins: 3, + podiums: 4, + isActive: true, + rank: 2, + avatarUrl: 'avatar-driver2', + }, + ], + totalRaces: 18, + totalWins: 8, + activeCount: 2, }); }); @@ -87,16 +117,16 @@ describe('GetDriversLeaderboardUseCase', () => { ); mockDriverFindAll.mockResolvedValue([]); - mockRankingGetAllDriverRankings.mockReturnValue({}); + mockRankingGetAllDriverRankings.mockReturnValue([]); const result = await useCase.execute(); expect(result.isOk()).toBe(true); expect(result.value).toEqual({ drivers: [], - rankings: {}, - stats: {}, - avatarUrls: {}, + totalRaces: 0, + totalWins: 0, + activeCount: 0, }); }); @@ -109,8 +139,8 @@ describe('GetDriversLeaderboardUseCase', () => { mockLogger, ); - const driver1 = { id: 'driver1', name: 'Driver One' }; - const rankings = { driver1: 1 }; + const driver1 = { id: 'driver1', name: { value: 'Driver One' }, country: { value: 'US' } }; + const rankings = [{ driverId: 'driver1', rating: 2500, overallRank: 1 }]; mockDriverFindAll.mockResolvedValue([driver1]); mockRankingGetAllDriverRankings.mockReturnValue(rankings); @@ -121,10 +151,24 @@ describe('GetDriversLeaderboardUseCase', () => { expect(result.isOk()).toBe(true); expect(result.value).toEqual({ - drivers: [driver1], - rankings, - stats: {}, - avatarUrls: { driver1: 'avatar-driver1' }, + drivers: [ + { + id: 'driver1', + name: 'Driver One', + rating: 2500, + skillLevel: 'Pro', + nationality: 'US', + racesCompleted: 0, + wins: 0, + podiums: 0, + isActive: true, + rank: 1, + avatarUrl: 'avatar-driver1', + }, + ], + totalRaces: 0, + totalWins: 0, + activeCount: 1, }); }); diff --git a/core/racing/application/use-cases/GetDriversLeaderboardUseCase.ts b/core/racing/application/use-cases/GetDriversLeaderboardUseCase.ts index 5bd14b051..1544197aa 100644 --- a/core/racing/application/use-cases/GetDriversLeaderboardUseCase.ts +++ b/core/racing/application/use-cases/GetDriversLeaderboardUseCase.ts @@ -3,7 +3,8 @@ import type { IRankingService } from '../../domain/services/IRankingService'; import type { IDriverStatsService } from '../../domain/services/IDriverStatsService'; import type { GetDriverAvatarInputPort } from '../ports/input/GetDriverAvatarInputPort'; import type { GetDriverAvatarOutputPort } from '../ports/output/GetDriverAvatarOutputPort'; -import type { DriversLeaderboardResultDTO } from '../presenters/IDriversLeaderboardPresenter'; +import type { DriversLeaderboardOutputPort, DriverLeaderboardItemOutputPort } from '../ports/output/DriversLeaderboardOutputPort'; +import type { SkillLevel } from '../../domain/services/SkillLevelService'; import type { AsyncUseCase, Logger } from '@core/shared/application'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; @@ -13,7 +14,7 @@ import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorC * Orchestrates domain logic and returns result. */ export class GetDriversLeaderboardUseCase - implements AsyncUseCase + implements AsyncUseCase { constructor( private readonly driverRepository: IDriverRepository, @@ -23,34 +24,52 @@ export class GetDriversLeaderboardUseCase private readonly logger: Logger, ) {} - async execute(): Promise>> { + async execute(): Promise>> { this.logger.debug('Executing GetDriversLeaderboardUseCase'); try { const drivers = await this.driverRepository.findAll(); const rankings = this.rankingService.getAllDriverRankings(); - const stats: DriversLeaderboardResultDTO['stats'] = {}; - const avatarUrls: DriversLeaderboardResultDTO['avatarUrls'] = {}; + const avatarUrls: Record = {}; for (const driver of drivers) { - const driverStats = this.driverStatsService.getDriverStats(driver.id); - if (driverStats) { - stats[driver.id] = driverStats; - } - const avatarResult = await this.getDriverAvatar({ driverId: driver.id }); avatarUrls[driver.id] = avatarResult.avatarUrl; } - const dto: DriversLeaderboardResultDTO = { - drivers, - rankings, - stats, - avatarUrls, + const driverItems: DriverLeaderboardItemOutputPort[] = drivers.map(driver => { + const ranking = rankings.find(r => r.driverId === driver.id); + const stats = this.driverStatsService.getDriverStats(driver.id); + + return { + id: driver.id, + name: driver.name.value, + rating: ranking?.rating ?? 0, + skillLevel: 'Pro' as SkillLevel, // TODO: map from domain + nationality: driver.country.value, + racesCompleted: stats?.totalRaces ?? 0, + wins: stats?.wins ?? 0, + podiums: stats?.podiums ?? 0, + isActive: true, // TODO: determine from domain + rank: ranking?.overallRank ?? 0, + avatarUrl: avatarUrls[driver.id], + }; + }); + + // Calculate totals + const totalRaces = driverItems.reduce((sum, d) => sum + d.racesCompleted, 0); + const totalWins = driverItems.reduce((sum, d) => sum + d.wins, 0); + const activeCount = driverItems.filter(d => d.isActive).length; + + const result: DriversLeaderboardOutputPort = { + drivers: driverItems.sort((a, b) => b.rating - a.rating), + totalRaces, + totalWins, + activeCount, }; this.logger.debug('Successfully retrieved drivers leaderboard.'); - return Result.ok(dto); + return Result.ok(result); } catch (error) { this.logger.error('Error executing GetDriversLeaderboardUseCase', error instanceof Error ? error : new Error(String(error))); return Result.err({ diff --git a/core/racing/application/use-cases/GetLeagueDriverSeasonStatsUseCase.test.ts b/core/racing/application/use-cases/GetLeagueDriverSeasonStatsUseCase.test.ts index cb72ed072..9857df206 100644 --- a/core/racing/application/use-cases/GetLeagueDriverSeasonStatsUseCase.test.ts +++ b/core/racing/application/use-cases/GetLeagueDriverSeasonStatsUseCase.test.ts @@ -4,6 +4,8 @@ import type { IStandingRepository } from '../../domain/repositories/IStandingRep import type { IResultRepository } from '../../domain/repositories/IResultRepository'; import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository'; import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; +import type { IDriverRepository } from '../../domain/repositories/IDriverRepository'; +import type { ITeamRepository } from '../../domain/repositories/ITeamRepository'; import type { DriverRatingPort } from '../ports/DriverRatingPort'; describe('GetLeagueDriverSeasonStatsUseCase', () => { @@ -12,12 +14,16 @@ describe('GetLeagueDriverSeasonStatsUseCase', () => { const mockPenaltyFindByRaceId = vi.fn(); const mockRaceFindByLeagueId = vi.fn(); const mockDriverRatingGetRating = vi.fn(); + const mockDriverFindById = vi.fn(); + const mockTeamFindById = vi.fn(); let useCase: GetLeagueDriverSeasonStatsUseCase; let standingRepository: IStandingRepository; let resultRepository: IResultRepository; let penaltyRepository: IPenaltyRepository; let raceRepository: IRaceRepository; + let driverRepository: IDriverRepository; + let teamRepository: ITeamRepository; let driverRatingPort: DriverRatingPort; beforeEach(() => { @@ -51,6 +57,12 @@ describe('GetLeagueDriverSeasonStatsUseCase', () => { delete: vi.fn(), exists: vi.fn(), }; + driverRepository = { + findById: mockDriverFindById, + }; + teamRepository = { + findById: mockTeamFindById, + }; driverRatingPort = { getRating: mockDriverRatingGetRating, }; @@ -60,6 +72,8 @@ describe('GetLeagueDriverSeasonStatsUseCase', () => { resultRepository, penaltyRepository, raceRepository, + driverRepository, + teamRepository, driverRatingPort, ); }); @@ -80,6 +94,8 @@ describe('GetLeagueDriverSeasonStatsUseCase', () => { ]; const mockResults = [{ position: 1 }]; const mockRating = { rating: 1500, ratingChange: 50 }; + const mockDriver = { id: 'driver-1', name: 'Driver One', teamId: 'team-1' }; + const mockTeam = { id: 'team-1', name: 'Team One' }; standingRepository.findByLeagueId.mockResolvedValue(mockStandings); raceRepository.findByLeagueId.mockResolvedValue(mockRaces); @@ -89,19 +105,39 @@ describe('GetLeagueDriverSeasonStatsUseCase', () => { }); driverRatingPort.getRating.mockReturnValue(mockRating); resultRepository.findByDriverIdAndLeagueId.mockResolvedValue(mockResults); + driverRepository.findById.mockImplementation((id) => { + if (id === 'driver-1') return Promise.resolve(mockDriver); + if (id === 'driver-2') return Promise.resolve({ id: 'driver-2', name: 'Driver Two' }); + return Promise.resolve(null); + }); + teamRepository.findById.mockResolvedValue(mockTeam); const result = await useCase.execute(params); expect(result.isOk()).toBe(true); - const dto = result.value!; - expect(dto.leagueId).toBe('league-1'); - expect(dto.standings).toEqual([ - { driverId: 'driver-1', position: 1, points: 100, racesCompleted: 5 }, - { driverId: 'driver-2', position: 2, points: 80, racesCompleted: 5 }, - ]); - expect(dto.penalties.get('driver-1')).toEqual({ baseDelta: -10, bonusDelta: 0 }); - expect(dto.driverRatings.get('driver-1')).toEqual(mockRating); - expect(dto.driverResults.get('driver-1')).toEqual(mockResults); + const output = result.value!; + expect(output.leagueId).toBe('league-1'); + expect(output.stats).toHaveLength(2); + expect(output.stats[0]).toEqual({ + leagueId: 'league-1', + driverId: 'driver-1', + position: 1, + driverName: 'Driver One', + teamId: 'team-1', + teamName: 'Team One', + totalPoints: 100, + basePoints: 90, + penaltyPoints: -10, + bonusPoints: 0, + pointsPerRace: 20, + racesStarted: 1, + racesFinished: 1, + dnfs: 0, + noShows: 1, + avgFinish: 1, + rating: 1500, + ratingChange: 50, + }); }); it('should handle no penalties', async () => { @@ -111,17 +147,20 @@ describe('GetLeagueDriverSeasonStatsUseCase', () => { const mockRaces = [{ id: 'race-1' }]; const mockResults = [{ position: 1 }]; const mockRating = { rating: null, ratingChange: null }; + const mockDriver = { id: 'driver-1', name: 'Driver One' }; standingRepository.findByLeagueId.mockResolvedValue(mockStandings); raceRepository.findByLeagueId.mockResolvedValue(mockRaces); penaltyRepository.findByRaceId.mockResolvedValue([]); driverRatingPort.getRating.mockReturnValue(mockRating); resultRepository.findByDriverIdAndLeagueId.mockResolvedValue(mockResults); + driverRepository.findById.mockResolvedValue(mockDriver); + teamRepository.findById.mockResolvedValue(null); const result = await useCase.execute(params); expect(result.isOk()).toBe(true); - const dto = result.value!; - expect(dto.penalties.size).toBe(0); + const output = result.value!; + expect(output.stats[0].penaltyPoints).toBe(0); }); }); \ No newline at end of file diff --git a/core/racing/application/use-cases/GetLeagueDriverSeasonStatsUseCase.ts b/core/racing/application/use-cases/GetLeagueDriverSeasonStatsUseCase.ts index a51d4ba10..350119039 100644 --- a/core/racing/application/use-cases/GetLeagueDriverSeasonStatsUseCase.ts +++ b/core/racing/application/use-cases/GetLeagueDriverSeasonStatsUseCase.ts @@ -2,7 +2,9 @@ import type { IStandingRepository } from '../../domain/repositories/IStandingRep import type { IResultRepository } from '../../domain/repositories/IResultRepository'; import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository'; import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; -import type { LeagueDriverSeasonStatsResultDTO } from '../presenters/ILeagueDriverSeasonStatsPresenter'; +import type { IDriverRepository } from '../../domain/repositories/IDriverRepository'; +import type { ITeamRepository } from '../../domain/repositories/ITeamRepository'; +import type { LeagueDriverSeasonStatsOutputPort } from '../ports/output/LeagueDriverSeasonStatsOutputPort'; import type { AsyncUseCase } from '@core/shared/application'; import { Result } from '@core/shared/application/Result'; import type { DriverRatingPort } from '../ports/DriverRatingPort'; @@ -11,16 +13,18 @@ import type { DriverRatingPort } from '../ports/DriverRatingPort'; * Use Case for retrieving league driver season statistics. * Orchestrates domain logic and returns the result. */ -export class GetLeagueDriverSeasonStatsUseCase implements AsyncUseCase<{ leagueId: string }, LeagueDriverSeasonStatsResultDTO, 'NO_ERROR'> { +export class GetLeagueDriverSeasonStatsUseCase implements AsyncUseCase<{ leagueId: string }, LeagueDriverSeasonStatsOutputPort, 'NO_ERROR'> { constructor( private readonly standingRepository: IStandingRepository, private readonly resultRepository: IResultRepository, private readonly penaltyRepository: IPenaltyRepository, private readonly raceRepository: IRaceRepository, + private readonly driverRepository: IDriverRepository, + private readonly teamRepository: ITeamRepository, private readonly driverRatingPort: DriverRatingPort, ) {} - async execute(params: { leagueId: string }): Promise> { + async execute(params: { leagueId: string }): Promise> { const { leagueId } = params; // Get standings and races for the league @@ -69,19 +73,54 @@ export class GetLeagueDriverSeasonStatsUseCase implements AsyncUseCase<{ leagueI driverResults.set(standing.driverId, results); } - const dto: LeagueDriverSeasonStatsResultDTO = { - leagueId, - standings: standings.map(standing => ({ + // Fetch drivers and teams + const driverIds = standings.map(s => s.driverId); + const drivers = await Promise.all(driverIds.map(id => this.driverRepository.findById(id))); + const driversMap = new Map(drivers.filter(d => d).map(d => [d!.id, d!])); + const teamIds = Array.from(new Set(drivers.filter(d => d?.teamId).map(d => d!.teamId!))); + const teams = await Promise.all(teamIds.map(id => this.teamRepository.findById(id))); + const teamsMap = new Map(teams.filter(t => t).map(t => [t!.id, t!])); + + // Compute stats + const stats = standings.map(standing => { + const driver = driversMap.get(standing.driverId); + const team = driver?.teamId ? teamsMap.get(driver.teamId) : undefined; + const penalties = penaltiesByDriver.get(standing.driverId) ?? { baseDelta: 0, bonusDelta: 0 }; + const results = driverResults.get(standing.driverId) ?? []; + const rating = driverRatings.get(standing.driverId); + + const racesStarted = results.length; + const racesFinished = results.filter(r => r.position > 0).length; + const dnfs = results.filter(r => r.position === 0).length; + const noShows = races.length - racesStarted; + const avgFinish = results.length > 0 ? results.reduce((sum, r) => sum + r.position, 0) / results.length : null; + const pointsPerRace = racesStarted > 0 ? standing.points / racesStarted : 0; + + return { + leagueId, driverId: standing.driverId, position: standing.position, - points: standing.points, - racesCompleted: standing.racesCompleted, - })), - penalties: penaltiesByDriver, - driverResults, - driverRatings, - }; + driverName: driver?.name ?? '', + teamId: driver?.teamId ?? undefined, + teamName: team?.name ?? undefined, + totalPoints: standing.points, + basePoints: standing.points - penalties.baseDelta, + penaltyPoints: penalties.baseDelta, + bonusPoints: penalties.bonusDelta, + pointsPerRace, + racesStarted, + racesFinished, + dnfs, + noShows, + avgFinish, + rating: rating?.rating ?? null, + ratingChange: rating?.ratingChange ?? null, + }; + }); - return Result.ok(dto); + return Result.ok({ + leagueId, + stats, + }); } } \ No newline at end of file diff --git a/core/racing/application/use-cases/GetLeagueFullConfigUseCase.test.ts b/core/racing/application/use-cases/GetLeagueFullConfigUseCase.test.ts index 873a86306..1483b2bef 100644 --- a/core/racing/application/use-cases/GetLeagueFullConfigUseCase.test.ts +++ b/core/racing/application/use-cases/GetLeagueFullConfigUseCase.test.ts @@ -4,7 +4,7 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository'; import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository'; import type { IGameRepository } from '../../domain/repositories/IGameRepository'; -import type { ILeagueFullConfigPresenter, LeagueConfigFormViewModel } from '../presenters/ILeagueFullConfigPresenter'; +import type { LeagueFullConfigOutputPort } from '../ports/output/LeagueFullConfigOutputPort'; describe('GetLeagueFullConfigUseCase', () => { let useCase: GetLeagueFullConfigUseCase; @@ -12,7 +12,6 @@ describe('GetLeagueFullConfigUseCase', () => { let seasonRepository: ISeasonRepository; let leagueScoringConfigRepository: ILeagueScoringConfigRepository; let gameRepository: IGameRepository; - let presenter: ILeagueFullConfigPresenter; beforeEach(() => { leagueRepository = { @@ -31,19 +30,12 @@ describe('GetLeagueFullConfigUseCase', () => { findById: vi.fn(), // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any; - presenter = { - reset: vi.fn(), - present: vi.fn(), - getViewModel: vi.fn(), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any; useCase = new GetLeagueFullConfigUseCase( leagueRepository, seasonRepository, leagueScoringConfigRepository, gameRepository, - presenter, ); }); @@ -71,63 +63,17 @@ describe('GetLeagueFullConfigUseCase', () => { const mockSeasons = [{ id: 'season-1', status: 'active', gameId: 'game-1' }]; const mockScoringConfig = { id: 'config-1' }; const mockGame = { id: 'game-1' }; - const mockViewModel: LeagueConfigFormViewModel = { - leagueId: 'league-1', - basics: { - name: 'Test League', - description: 'A test league', - visibility: 'public', - gameId: 'iracing', - }, - structure: { - mode: 'solo', - maxDrivers: 32, - multiClassEnabled: false, - }, - championships: { - enableDriverChampionship: true, - enableTeamChampionship: false, - enableNationsChampionship: false, - enableTrophyChampionship: false, - }, - scoring: { - customScoringEnabled: false, - }, - dropPolicy: { - strategy: 'none', - }, - timings: { - practiceMinutes: 30, - qualifyingMinutes: 15, - mainRaceMinutes: 60, - sessionCount: 1, - roundsPlanned: 10, - }, - stewarding: { - decisionMode: 'admin_only', - requireDefense: false, - defenseTimeLimit: 48, - voteTimeLimit: 72, - protestDeadlineHours: 48, - stewardingClosesHours: 168, - notifyAccusedOnProtest: true, - notifyOnVoteRequired: true, - }, - }; leagueRepository.findById.mockResolvedValue(mockLeague); seasonRepository.findByLeagueId.mockResolvedValue(mockSeasons); leagueScoringConfigRepository.findBySeasonId.mockResolvedValue(mockScoringConfig); gameRepository.findById.mockResolvedValue(mockGame); - presenter.getViewModel.mockReturnValue(mockViewModel); const result = await useCase.execute(params); expect(result.isOk()).toBe(true); - const viewModel = result.value!; - expect(viewModel).toEqual(mockViewModel); - expect(presenter.reset).toHaveBeenCalled(); - expect(presenter.present).toHaveBeenCalledWith({ + const output = result.value!; + expect(output).toEqual({ league: mockLeague, activeSeason: mockSeasons[0], scoringConfig: mockScoringConfig, @@ -157,25 +103,15 @@ describe('GetLeagueFullConfigUseCase', () => { description: 'A test league', settings: { maxDrivers: 32 }, }; - const mockViewModel: LeagueConfigFormViewModel = { - leagueId: 'league-1', - basics: { name: 'Test League', description: 'A test league', visibility: 'public', gameId: 'iracing' }, - structure: { mode: 'solo', maxDrivers: 32, multiClassEnabled: false }, - championships: { enableDriverChampionship: true, enableTeamChampionship: false, enableNationsChampionship: false, enableTrophyChampionship: false }, - scoring: { customScoringEnabled: false }, - dropPolicy: { strategy: 'none' }, - timings: { practiceMinutes: 30, qualifyingMinutes: 15, mainRaceMinutes: 60, sessionCount: 1, roundsPlanned: 10 }, - stewarding: { decisionMode: 'admin_only', requireDefense: false, defenseTimeLimit: 48, voteTimeLimit: 72, protestDeadlineHours: 48, stewardingClosesHours: 168, notifyAccusedOnProtest: true, notifyOnVoteRequired: true }, - }; leagueRepository.findById.mockResolvedValue(mockLeague); seasonRepository.findByLeagueId.mockResolvedValue([]); - presenter.getViewModel.mockReturnValue(mockViewModel); const result = await useCase.execute(params); expect(result.isOk()).toBe(true); - expect(presenter.present).toHaveBeenCalledWith({ + const output = result.value!; + expect(output).toEqual({ league: mockLeague, }); }); diff --git a/core/racing/application/use-cases/GetLeagueFullConfigUseCase.ts b/core/racing/application/use-cases/GetLeagueFullConfigUseCase.ts index bc0c6cf05..2e054c99e 100644 --- a/core/racing/application/use-cases/GetLeagueFullConfigUseCase.ts +++ b/core/racing/application/use-cases/GetLeagueFullConfigUseCase.ts @@ -2,29 +2,24 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository'; import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository'; import type { IGameRepository } from '../../domain/repositories/IGameRepository'; -import type { - ILeagueFullConfigPresenter, - LeagueFullConfigData, - LeagueConfigFormViewModel, -} from '../presenters/ILeagueFullConfigPresenter'; +import type { LeagueFullConfigOutputPort } from '../ports/output/LeagueFullConfigOutputPort'; import type { AsyncUseCase } from '@core/shared/application'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; /** * Use Case for retrieving a league's full configuration. - * Orchestrates domain logic and delegates presentation to the presenter. + * Orchestrates domain logic and returns the configuration data. */ -export class GetLeagueFullConfigUseCase implements AsyncUseCase<{ leagueId: string }, LeagueConfigFormViewModel, 'LEAGUE_NOT_FOUND' | 'PRESENTATION_FAILED'> { +export class GetLeagueFullConfigUseCase implements AsyncUseCase<{ leagueId: string }, LeagueFullConfigOutputPort, 'LEAGUE_NOT_FOUND'> { constructor( private readonly leagueRepository: ILeagueRepository, private readonly seasonRepository: ISeasonRepository, private readonly leagueScoringConfigRepository: ILeagueScoringConfigRepository, private readonly gameRepository: IGameRepository, - private readonly presenter: ILeagueFullConfigPresenter, ) {} - async execute(params: { leagueId: string }): Promise>> { + async execute(params: { leagueId: string }): Promise>> { const { leagueId } = params; const league = await this.leagueRepository.findById(leagueId); @@ -47,20 +42,13 @@ export class GetLeagueFullConfigUseCase implements AsyncUseCase<{ leagueId: stri return this.gameRepository.findById(activeSeason.gameId); })(); - const data: LeagueFullConfigData = { + const output: LeagueFullConfigOutputPort = { league, ...(activeSeason ? { activeSeason } : {}), ...(scoringConfig ? { scoringConfig } : {}), ...(game ? { game } : {}), }; - this.presenter.reset(); - this.presenter.present(data); - const viewModel = this.presenter.getViewModel(); - if (!viewModel) { - return Result.err({ code: 'PRESENTATION_FAILED', details: { message: 'Failed to present league config' } }); - } - - return Result.ok(viewModel); + return Result.ok(output); } } \ No newline at end of file diff --git a/core/racing/application/use-cases/GetLeagueOwnerSummaryUseCase.ts b/core/racing/application/use-cases/GetLeagueOwnerSummaryUseCase.ts index b4777eb7c..d1077ccad 100644 --- a/core/racing/application/use-cases/GetLeagueOwnerSummaryUseCase.ts +++ b/core/racing/application/use-cases/GetLeagueOwnerSummaryUseCase.ts @@ -13,7 +13,7 @@ export class GetLeagueOwnerSummaryUseCase implements AsyncUseCase>> { const driver = await this.driverRepository.findById(params.ownerId); - const summary = driver ? { driver: { id: driver.id, name: driver.name }, rating: 0, rank: 0 } : null; + const summary = driver ? { driver: { id: driver.id, iracingId: driver.iracingId.toString(), name: driver.name.toString(), country: driver.country.toString(), bio: driver.bio?.toString(), joinedAt: driver.joinedAt.toDate().toISOString() }, rating: 0, rank: 0 } : null; return Result.ok({ summary }); } } \ No newline at end of file diff --git a/core/racing/application/use-cases/GetLeagueProtestsUseCase.ts b/core/racing/application/use-cases/GetLeagueProtestsUseCase.ts index 5a7e60f24..3a832b91b 100644 --- a/core/racing/application/use-cases/GetLeagueProtestsUseCase.ts +++ b/core/racing/application/use-cases/GetLeagueProtestsUseCase.ts @@ -4,27 +4,41 @@ import type { IDriverRepository } from '../../domain/repositories/IDriverReposit import type { AsyncUseCase } from '@core/shared/application'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; -import type { GetLeagueProtestsResultDTO, ProtestDTO } from '../dto/GetLeagueProtestsResultDTO'; +import type { GetLeagueProtestsOutputPort, ProtestOutputPort, RaceOutputPort, DriverOutputPort } from '../ports/output/GetLeagueProtestsOutputPort'; export interface GetLeagueProtestsUseCaseParams { leagueId: string; } -export class GetLeagueProtestsUseCase implements AsyncUseCase { +export class GetLeagueProtestsUseCase implements AsyncUseCase { constructor( private readonly raceRepository: IRaceRepository, private readonly protestRepository: IProtestRepository, private readonly driverRepository: IDriverRepository, ) {} - async execute(params: GetLeagueProtestsUseCaseParams): Promise>> { + async execute(params: GetLeagueProtestsUseCaseParams): Promise>> { const races = await this.raceRepository.findByLeagueId(params.leagueId); - const protests: ProtestDTO[] = []; - const raceMap = new Map(); + const protests: ProtestOutputPort[] = []; + const racesById: Record = {}; + const driversById: Record = {}; const driverIds = new Set(); for (const race of races) { - raceMap.set(race.id, { id: race.id, name: race.track, date: race.scheduledAt.toISOString() }); + racesById[race.id] = { + id: race.id, + leagueId: race.leagueId, + scheduledAt: race.scheduledAt.toISOString(), + track: race.track, + trackId: race.trackId, + car: race.car, + carId: race.carId, + sessionType: race.sessionType.toString(), + status: race.status, + strengthOfField: race.strengthOfField, + registeredCount: race.registeredCount, + maxParticipants: race.maxParticipants, + }; const raceProtests = await this.protestRepository.findByRaceId(race.id); for (const protest of raceProtests) { protests.push({ @@ -32,26 +46,48 @@ export class GetLeagueProtestsUseCase implements AsyncUseCase { +export class GetLeagueScheduleUseCase implements AsyncUseCase { constructor(private readonly raceRepository: IRaceRepository) {} - async execute(params: GetLeagueScheduleUseCaseParams): Promise>> { + async execute(params: GetLeagueScheduleUseCaseParams): Promise>> { const races = await this.raceRepository.findByLeagueId(params.leagueId); return Result.ok({ races: races.map(race => ({ diff --git a/core/racing/application/use-cases/GetLeagueScoringConfigUseCase.ts b/core/racing/application/use-cases/GetLeagueScoringConfigUseCase.ts index ca52ff49b..595ce5f72 100644 --- a/core/racing/application/use-cases/GetLeagueScoringConfigUseCase.ts +++ b/core/racing/application/use-cases/GetLeagueScoringConfigUseCase.ts @@ -4,7 +4,7 @@ import type { ILeagueScoringConfigRepository } from '../../domain/repositories/I import type { IGameRepository } from '../../domain/repositories/IGameRepository'; import type { GetLeagueScoringPresetByIdInputPort } from '../ports/input/GetLeagueScoringPresetByIdInputPort'; import type { LeagueScoringPresetOutputPort } from '../ports/output/LeagueScoringPresetOutputPort'; -import type { LeagueScoringConfigData } from '../presenters/ILeagueScoringConfigPresenter'; +import type { LeagueScoringConfigOutputPort } from '../ports/output/LeagueScoringConfigOutputPort'; import type { AsyncUseCase } from '@core/shared/application/AsyncUseCase'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; @@ -20,7 +20,7 @@ type GetLeagueScoringConfigErrorCode = * Use Case for retrieving a league's scoring configuration for its active season. */ export class GetLeagueScoringConfigUseCase - implements AsyncUseCase<{ leagueId: string }, LeagueScoringConfigData, GetLeagueScoringConfigErrorCode> + implements AsyncUseCase<{ leagueId: string }, LeagueScoringConfigOutputPort, GetLeagueScoringConfigErrorCode> { constructor( private readonly leagueRepository: ILeagueRepository, @@ -30,7 +30,7 @@ export class GetLeagueScoringConfigUseCase private readonly getLeagueScoringPresetById: (input: GetLeagueScoringPresetByIdInputPort) => Promise, ) {} - async execute(params: { leagueId: string }): Promise>> { + async execute(params: { leagueId: string }): Promise>> { const { leagueId } = params; const league = await this.leagueRepository.findById(leagueId); @@ -64,7 +64,7 @@ export class GetLeagueScoringConfigUseCase const presetId = scoringConfig.scoringPresetId; const preset = presetId ? await this.getLeagueScoringPresetById({ presetId }) : undefined; - const data: LeagueScoringConfigData = { + const output: LeagueScoringConfigOutputPort = { leagueId: league.id, seasonId: activeSeason.id, gameId: game.id, @@ -74,6 +74,6 @@ export class GetLeagueScoringConfigUseCase championships: scoringConfig.championships, }; - return Result.ok(data); + return Result.ok(output); } } \ No newline at end of file diff --git a/core/racing/application/use-cases/GetLeagueSeasonsUseCase.ts b/core/racing/application/use-cases/GetLeagueSeasonsUseCase.ts index 0224156d6..feaf328e0 100644 --- a/core/racing/application/use-cases/GetLeagueSeasonsUseCase.ts +++ b/core/racing/application/use-cases/GetLeagueSeasonsUseCase.ts @@ -1,5 +1,5 @@ import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository'; -import type { GetLeagueSeasonsViewModel } from '../presenters/IGetLeagueSeasonsPresenter'; +import type { GetLeagueSeasonsOutputPort } from '../ports/output/GetLeagueSeasonsOutputPort'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; @@ -10,11 +10,11 @@ export interface GetLeagueSeasonsUseCaseParams { export class GetLeagueSeasonsUseCase { constructor(private readonly seasonRepository: ISeasonRepository) {} - async execute(params: GetLeagueSeasonsUseCaseParams): Promise>> { + async execute(params: GetLeagueSeasonsUseCaseParams): Promise>> { try { const seasons = await this.seasonRepository.findByLeagueId(params.leagueId); const activeCount = seasons.filter(s => s.status === 'active').length; - const viewModel: GetLeagueSeasonsViewModel = { + const output: GetLeagueSeasonsOutputPort = { seasons: seasons.map(s => ({ seasonId: s.id, name: s.name, @@ -25,7 +25,7 @@ export class GetLeagueSeasonsUseCase { isParallelActive: s.status === 'active' && activeCount > 1 })) }; - return Result.ok(viewModel); + return Result.ok(output); } catch { return Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'Failed to fetch seasons' } }); } diff --git a/core/racing/application/use-cases/GetLeagueStandingsUseCase.ts b/core/racing/application/use-cases/GetLeagueStandingsUseCase.ts index 0c4ff7234..5764ed8b9 100644 --- a/core/racing/application/use-cases/GetLeagueStandingsUseCase.ts +++ b/core/racing/application/use-cases/GetLeagueStandingsUseCase.ts @@ -1,6 +1,6 @@ import type { IStandingRepository } from '../../domain/repositories/IStandingRepository'; import type { IDriverRepository } from '../../domain/repositories/IDriverRepository'; -import type { LeagueStandingsViewModel } from '../presenters/ILeagueStandingsPresenter'; +import type { LeagueStandingsOutputPort } from '../ports/output/LeagueStandingsOutputPort'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; @@ -19,7 +19,7 @@ export class GetLeagueStandingsUseCase { async execute( params: GetLeagueStandingsUseCaseParams, - ): Promise>> { + ): Promise>> { try { const standings = await this.standingRepository.findByLeagueId(params.leagueId); const driverIds = [...new Set(standings.map(s => s.driverId))]; @@ -27,7 +27,7 @@ export class GetLeagueStandingsUseCase { const driverResults = await Promise.all(driverPromises); const drivers = driverResults.filter((d): d is NonNullable => d !== null); const driverMap = new Map(drivers.map(d => [d.id, { id: d.id, name: d.name }])); - const viewModel: LeagueStandingsViewModel = { + const viewModel: LeagueStandingsOutputPort = { standings: standings.map(s => ({ driverId: s.driverId, driver: driverMap.get(s.driverId)!, diff --git a/core/racing/application/use-cases/GetLeagueStatsUseCase.ts b/core/racing/application/use-cases/GetLeagueStatsUseCase.ts index d139d2fff..daeafda2a 100644 --- a/core/racing/application/use-cases/GetLeagueStatsUseCase.ts +++ b/core/racing/application/use-cases/GetLeagueStatsUseCase.ts @@ -1,6 +1,6 @@ import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository'; import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; -import type { LeagueStatsViewModel } from '../presenters/ILeagueStatsPresenter'; +import type { LeagueStatsOutputPort } from '../ports/output/LeagueStatsOutputPort'; import type { GetDriverRatingInputPort } from '../ports/input/GetDriverRatingInputPort'; import type { GetDriverRatingOutputPort } from '../ports/output/GetDriverRatingOutputPort'; import { Result } from '@core/shared/application/Result'; @@ -17,7 +17,7 @@ export class GetLeagueStatsUseCase { private readonly getDriverRating: (input: GetDriverRatingInputPort) => Promise, ) {} - async execute(params: GetLeagueStatsUseCaseParams): Promise>> { + async execute(params: GetLeagueStatsUseCaseParams): Promise>> { try { const memberships = await this.leagueMembershipRepository.getLeagueMembers(params.leagueId); const races = await this.raceRepository.findByLeagueId(params.leagueId); @@ -35,7 +35,7 @@ export class GetLeagueStatsUseCase { const averageRating = validRatings.length > 0 ? Math.round(validRatings.reduce((sum, r) => sum + r, 0) / validRatings.length) : 0; - const viewModel: LeagueStatsViewModel = { + const viewModel: LeagueStatsOutputPort = { totalMembers: memberships.length, totalRaces: races.length, averageRating, diff --git a/core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase.ts b/core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase.ts index d6a9e19fb..c89ada239 100644 --- a/core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase.ts +++ b/core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase.ts @@ -8,7 +8,7 @@ import type { ISponsorshipRequestRepository } from '../../domain/repositories/IS import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository'; import type { SponsorableEntityType } from '../../domain/entities/SponsorshipRequest'; import type { SponsorshipTier } from '../../domain/entities/SeasonSponsorship'; -import type { PendingSponsorshipRequestsViewModel } from '../presenters/IPendingSponsorshipRequestsPresenter'; +import type { PendingSponsorshipRequestsOutputPort } from '../ports/output/PendingSponsorshipRequestsOutputPort'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; @@ -47,7 +47,7 @@ export class GetPendingSponsorshipRequestsUseCase { async execute( dto: GetPendingSponsorshipRequestsDTO, - ): Promise>> { + ): Promise>> { try { const requests = await this.sponsorshipRequestRepo.findPendingByEntity( dto.entityType, @@ -78,13 +78,13 @@ export class GetPendingSponsorshipRequestsUseCase { // Sort by creation date (newest first) requestDTOs.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); - const viewModel: PendingSponsorshipRequestsViewModel = { + const outputPort: PendingSponsorshipRequestsOutputPort = { entityType: dto.entityType, entityId: dto.entityId, requests: requestDTOs, totalCount: requestDTOs.length, }; - return Result.ok(viewModel); + return Result.ok(outputPort); } catch { return Result.err({ code: 'REPOSITORY_ERROR', message: 'Failed to fetch pending sponsorship requests' }); } diff --git a/core/racing/application/use-cases/GetProfileOverviewUseCase.ts b/core/racing/application/use-cases/GetProfileOverviewUseCase.ts index 4eb7c281f..773b8f873 100644 --- a/core/racing/application/use-cases/GetProfileOverviewUseCase.ts +++ b/core/racing/application/use-cases/GetProfileOverviewUseCase.ts @@ -5,14 +5,7 @@ import type { IImageServicePort } from '../ports/IImageServicePort'; import type { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository'; import type { Driver } from '../../domain/entities/Driver'; import type { Team } from '../../domain/entities/Team'; -import type { - ProfileOverviewViewModel, - ProfileOverviewDriverSummaryViewModel, - ProfileOverviewStatsViewModel, - ProfileOverviewFinishDistributionViewModel, - ProfileOverviewTeamMembershipViewModel, - ProfileOverviewSocialSummaryViewModel, -} from '../presenters/IProfileOverviewPresenter'; +import type { ProfileOverviewOutputPort } from '../ports/output/ProfileOverviewOutputPort'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; @@ -51,7 +44,7 @@ export class GetProfileOverviewUseCase { private readonly getAllDriverRankings: () => DriverRankingEntry[], ) {} - async execute(params: GetProfileOverviewParams): Promise>> { + async execute(params: GetProfileOverviewParams): Promise>> { try { const { driverId } = params; @@ -73,8 +66,8 @@ export class GetProfileOverviewUseCase { const teamMemberships = await this.buildTeamMemberships(driver.id, teams as Team[]); const socialSummary = this.buildSocialSummary(friends as Driver[]); - const viewModel: ProfileOverviewViewModel = { - currentDriver: driverSummary, + const outputPort: ProfileOverviewOutputPort = { + driver: driverSummary, stats, finishDistribution, teamMemberships, @@ -82,7 +75,7 @@ export class GetProfileOverviewUseCase { extendedProfile: null, }; - return Result.ok(viewModel); + return Result.ok(outputPort); } catch { return Result.err({ code: 'REPOSITORY_ERROR', message: 'Failed to fetch profile overview' }); } @@ -91,25 +84,25 @@ export class GetProfileOverviewUseCase { private buildDriverSummary( driver: Driver, stats: ProfileDriverStatsAdapter | null, - ): ProfileOverviewDriverSummaryViewModel { + ): ProfileOverviewOutputPort['driver'] { const rankings = this.getAllDriverRankings(); const fallbackRank = this.computeFallbackRank(driver.id, rankings); const totalDrivers = rankings.length; return { id: driver.id, - name: driver.name, - country: driver.country, + name: driver.name.value, + country: driver.country.value, avatarUrl: this.imageService.getDriverAvatar(driver.id), - iracingId: driver.iracingId ?? null, + iracingId: driver.iracingId?.value ?? null, joinedAt: driver.joinedAt instanceof Date - ? driver.joinedAt.toISOString() - : new Date(driver.joinedAt).toISOString(), + ? driver.joinedAt + : new Date(driver.joinedAt.value), rating: stats?.rating ?? null, globalRank: stats?.overallRank ?? fallbackRank, consistency: stats?.consistency ?? null, - bio: driver.bio ?? null, + bio: driver.bio?.value ?? null, totalDrivers, }; } @@ -193,8 +186,8 @@ export class GetProfileOverviewUseCase { private async buildTeamMemberships( driverId: string, teams: Team[], - ): Promise { - const memberships: ProfileOverviewTeamMembershipViewModel[] = []; + ): Promise { + const memberships: ProfileOverviewOutputPort['teamMemberships'] = []; for (const team of teams) { const membership = await this.teamMembershipRepository.getMembership( @@ -205,29 +198,29 @@ export class GetProfileOverviewUseCase { memberships.push({ teamId: team.id, - teamName: team.name, - teamTag: team.tag ?? null, + teamName: team.name.value, + teamTag: team.tag?.value ?? null, role: membership.role, joinedAt: membership.joinedAt instanceof Date - ? membership.joinedAt.toISOString() - : new Date(membership.joinedAt).toISOString(), + ? membership.joinedAt + : new Date(membership.joinedAt), isCurrent: membership.status === 'active', }); } - memberships.sort((a, b) => a.joinedAt.localeCompare(b.joinedAt)); + memberships.sort((a, b) => a.joinedAt.getTime() - b.joinedAt.getTime()); return memberships; } - private buildSocialSummary(friends: Driver[]): ProfileOverviewSocialSummaryViewModel { + private buildSocialSummary(friends: Driver[]): ProfileOverviewOutputPort['socialSummary'] { return { friendsCount: friends.length, friends: friends.map(friend => ({ id: friend.id, - name: friend.name, - country: friend.country, + name: friend.name.value, + country: friend.country.value, avatarUrl: this.imageService.getDriverAvatar(friend.id), })), }; diff --git a/core/racing/application/use-cases/GetRaceDetailUseCase.ts b/core/racing/application/use-cases/GetRaceDetailUseCase.ts index b8e9f7bc7..2727e84b6 100644 --- a/core/racing/application/use-cases/GetRaceDetailUseCase.ts +++ b/core/racing/application/use-cases/GetRaceDetailUseCase.ts @@ -4,17 +4,7 @@ import type { IDriverRepository } from '../../domain/repositories/IDriverReposit import type { IRaceRegistrationRepository } from '../../domain/repositories/IRaceRegistrationRepository'; import type { IResultRepository } from '../../domain/repositories/IResultRepository'; import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository'; -import type { GetDriverRatingInputPort } from '../ports/input/GetDriverRatingInputPort'; -import type { GetDriverRatingOutputPort } from '../ports/output/GetDriverRatingOutputPort'; -import type { GetDriverAvatarInputPort } from '../ports/input/GetDriverAvatarInputPort'; -import type { GetDriverAvatarOutputPort } from '../ports/output/GetDriverAvatarOutputPort'; -import type { - RaceDetailViewModel, - RaceDetailRaceViewModel, - RaceDetailLeagueViewModel, - RaceDetailEntryViewModel, - RaceDetailUserResultViewModel, -} from '../presenters/IRaceDetailPresenter'; +import type { RaceDetailOutputPort } from '../ports/output/RaceDetailOutputPort'; import type { AsyncUseCase } from '@core/shared/application/AsyncUseCase'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; @@ -37,7 +27,7 @@ export interface GetRaceDetailQueryParams { type GetRaceDetailErrorCode = 'RACE_NOT_FOUND'; export class GetRaceDetailUseCase - implements AsyncUseCase + implements AsyncUseCase { constructor( private readonly raceRepository: IRaceRepository, @@ -46,11 +36,9 @@ export class GetRaceDetailUseCase private readonly raceRegistrationRepository: IRaceRegistrationRepository, private readonly resultRepository: IResultRepository, private readonly leagueMembershipRepository: ILeagueMembershipRepository, - private readonly getDriverRating: (input: GetDriverRatingInputPort) => Promise, - private readonly getDriverAvatar: (input: GetDriverAvatarInputPort) => Promise, ) {} - async execute(params: GetRaceDetailQueryParams): Promise>> { + async execute(params: GetRaceDetailQueryParams): Promise>> { const { raceId, driverId } = params; const race = await this.raceRepository.findById(raceId); @@ -58,105 +46,40 @@ export class GetRaceDetailUseCase return Result.err({ code: 'RACE_NOT_FOUND' }); } - const [league, registeredDriverIds, membership] = await Promise.all([ + const [league, registrations, membership] = await Promise.all([ this.leagueRepository.findById(race.leagueId), - this.raceRegistrationRepository.getRegisteredDrivers(race.id), + this.raceRegistrationRepository.findByRaceId(race.id), this.leagueMembershipRepository.getMembership(race.leagueId, driverId), ]); const drivers = await Promise.all( - registeredDriverIds.map(id => this.driverRepository.findById(id)), + registrations.map(registration => this.driverRepository.findById(registration.driverId.toString())), ); - const entryList: RaceDetailEntryViewModel[] = []; - for (const driver of drivers) { - if (driver) { - const ratingResult = await this.getDriverRating({ driverId: driver.id }); - const avatarResult = await this.getDriverAvatar({ driverId: driver.id }); - - entryList.push({ - id: driver.id, - name: driver.name, - country: driver.country, - avatarUrl: avatarResult.avatarUrl, - rating: ratingResult.rating, - isCurrentUser: driver.id === driverId, - }); - } - } + const validDrivers = drivers.filter((driver): driver is NonNullable => driver !== null); - const isUserRegistered = registeredDriverIds.includes(driverId); + const isUserRegistered = registrations.some(reg => reg.driverId.toString() === driverId); const isUpcoming = race.status === 'scheduled' && race.scheduledAt > new Date(); const canRegister = !!membership && membership.status === 'active' && isUpcoming; - let userResultView: RaceDetailUserResultViewModel | null = null; + let userResult: Result | null = null; if (race.status === 'completed') { const results = await this.resultRepository.findByRaceId(race.id); - const userResult = results.find(r => r.driverId === driverId) ?? null; - - if (userResult) { - const ratingChange = this.calculateRatingChange(userResult.position); - - userResultView = { - position: userResult.position, - startPosition: userResult.startPosition, - incidents: userResult.incidents, - fastestLap: userResult.fastestLap, - positionChange: userResult.getPositionChange(), - isPodium: userResult.isPodium(), - isClean: userResult.isClean(), - ratingChange, - }; - } + userResult = results.find(r => r.driverId.toString() === driverId) ?? null; } - const raceView: RaceDetailRaceViewModel = { - id: race.id, - leagueId: race.leagueId, - track: race.track, - car: race.car, - scheduledAt: race.scheduledAt.toISOString(), - sessionType: race.sessionType, - status: race.status, - strengthOfField: race.strengthOfField ?? null, - ...(race.registeredCount !== undefined ? { registeredCount: race.registeredCount } : {}), - ...(race.maxParticipants !== undefined ? { maxParticipants: race.maxParticipants } : {}), + const outputPort: RaceDetailOutputPort = { + race, + league, + registrations, + drivers: validDrivers, + userResult, + isUserRegistered, + canRegister, }; - const leagueView: RaceDetailLeagueViewModel | null = league - ? { - id: league.id, - name: league.name, - description: league.description, - settings: { - ...(league.settings.maxDrivers !== undefined - ? { maxDrivers: league.settings.maxDrivers } - : {}), - ...(league.settings.qualifyingFormat !== undefined - ? { qualifyingFormat: league.settings.qualifyingFormat } - : {}), - }, - } - : null; - - const viewModel: RaceDetailViewModel = { - race: raceView, - league: leagueView, - entryList, - registration: { - isUserRegistered, - canRegister, - }, - userResult: userResultView, - }; - - return Result.ok(viewModel); + return Result.ok(outputPort); } - private calculateRatingChange(position: number): number { - const baseChange = position <= 3 ? 25 : position <= 10 ? 10 : -5; - const positionBonus = Math.max(0, (20 - position) * 2); - return baseChange + positionBonus; - } } \ No newline at end of file diff --git a/core/racing/application/use-cases/GetRacePenaltiesUseCase.ts b/core/racing/application/use-cases/GetRacePenaltiesUseCase.ts index 7f42e4f62..24effdec0 100644 --- a/core/racing/application/use-cases/GetRacePenaltiesUseCase.ts +++ b/core/racing/application/use-cases/GetRacePenaltiesUseCase.ts @@ -7,7 +7,7 @@ import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository'; import type { IDriverRepository } from '../../domain/repositories/IDriverRepository'; -import type { RacePenaltiesResultDTO } from '../presenters/IRacePenaltiesPresenter'; +import type { RacePenaltiesOutputPort } from '../ports/output/RacePenaltiesOutputPort'; import type { AsyncUseCase } from '@core/shared/application/AsyncUseCase'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; @@ -16,13 +16,13 @@ export interface GetRacePenaltiesInput { raceId: string; } -export class GetRacePenaltiesUseCase implements AsyncUseCase { +export class GetRacePenaltiesUseCase implements AsyncUseCase { constructor( private readonly penaltyRepository: IPenaltyRepository, private readonly driverRepository: IDriverRepository, ) {} - async execute(input: GetRacePenaltiesInput): Promise>> { + async execute(input: GetRacePenaltiesInput): Promise>> { const penalties = await this.penaltyRepository.findByRaceId(input.raceId); const driverIds = new Set(); @@ -35,17 +35,12 @@ export class GetRacePenaltiesUseCase implements AsyncUseCase this.driverRepository.findById(id)), ); - const driverMap = new Map(); - drivers.forEach((driver) => { - if (driver) { - driverMap.set(driver.id, driver.name); - } - }); + const validDrivers = drivers.filter((driver): driver is NonNullable => driver !== null); - const dto: RacePenaltiesResultDTO = { + const outputPort: RacePenaltiesOutputPort = { penalties, - driverMap, + drivers: validDrivers, }; - return Result.ok(dto); + return Result.ok(outputPort); } } \ No newline at end of file diff --git a/core/racing/application/use-cases/GetRaceProtestsUseCase.ts b/core/racing/application/use-cases/GetRaceProtestsUseCase.ts index 1562fc0ab..88940e79a 100644 --- a/core/racing/application/use-cases/GetRaceProtestsUseCase.ts +++ b/core/racing/application/use-cases/GetRaceProtestsUseCase.ts @@ -7,7 +7,7 @@ import type { IProtestRepository } from '../../domain/repositories/IProtestRepository'; import type { IDriverRepository } from '../../domain/repositories/IDriverRepository'; -import type { RaceProtestsResultDTO } from '../presenters/IRaceProtestsPresenter'; +import type { RaceProtestsOutputPort } from '../ports/output/RaceProtestsOutputPort'; import type { AsyncUseCase } from '@core/shared/application/AsyncUseCase'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; @@ -16,13 +16,13 @@ export interface GetRaceProtestsInput { raceId: string; } -export class GetRaceProtestsUseCase implements AsyncUseCase { +export class GetRaceProtestsUseCase implements AsyncUseCase { constructor( private readonly protestRepository: IProtestRepository, private readonly driverRepository: IDriverRepository, ) {} - async execute(input: GetRaceProtestsInput): Promise>> { + async execute(input: GetRaceProtestsInput): Promise>> { const protests = await this.protestRepository.findByRaceId(input.raceId); const driverIds = new Set(); @@ -38,17 +38,12 @@ export class GetRaceProtestsUseCase implements AsyncUseCase this.driverRepository.findById(id)), ); - const driverMap = new Map(); - drivers.forEach((driver) => { - if (driver) { - driverMap.set(driver.id, driver.name); - } - }); + const validDrivers = drivers.filter((driver): driver is NonNullable => driver !== null); - const dto: RaceProtestsResultDTO = { + const outputPort: RaceProtestsOutputPort = { protests, - driverMap, + drivers: validDrivers, }; - return Result.ok(dto); + return Result.ok(outputPort); } } \ No newline at end of file diff --git a/core/racing/application/use-cases/GetRaceRegistrationsUseCase.test.ts b/core/racing/application/use-cases/GetRaceRegistrationsUseCase.test.ts index 51e7e1163..05eead61e 100644 --- a/core/racing/application/use-cases/GetRaceRegistrationsUseCase.test.ts +++ b/core/racing/application/use-cases/GetRaceRegistrationsUseCase.test.ts @@ -1,38 +1,42 @@ import { describe, it, expect, beforeEach, vi, Mock } from 'vitest'; import { GetRaceRegistrationsUseCase } from './GetRaceRegistrationsUseCase'; import type { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository'; +import { RaceRegistration } from '@core/racing/domain/entities/RaceRegistration'; describe('GetRaceRegistrationsUseCase', () => { let useCase: GetRaceRegistrationsUseCase; - let registrationRepository: { getRegisteredDrivers: Mock }; + let registrationRepository: { findByRaceId: Mock }; beforeEach(() => { - registrationRepository = { getRegisteredDrivers: vi.fn() }; + registrationRepository = { findByRaceId: vi.fn() }; useCase = new GetRaceRegistrationsUseCase( registrationRepository as unknown as IRaceRegistrationRepository, ); }); - it('should return registered driver ids', async () => { + it('should return registrations', async () => { const raceId = 'race-1'; - const registeredDriverIds = ['driver-1', 'driver-2']; + const registrations = [ + RaceRegistration.create({ raceId, driverId: 'driver-1' }), + RaceRegistration.create({ raceId, driverId: 'driver-2' }), + ]; - registrationRepository.getRegisteredDrivers.mockResolvedValue(registeredDriverIds); + registrationRepository.findByRaceId.mockResolvedValue(registrations); const result = await useCase.execute({ raceId }); expect(result.isOk()).toBe(true); - const dto = result.unwrap(); - expect(dto.registeredDriverIds).toEqual(registeredDriverIds); + const outputPort = result.unwrap(); + expect(outputPort.registrations).toEqual(registrations); }); it('should return empty array when no registrations', async () => { - registrationRepository.getRegisteredDrivers.mockResolvedValue([]); + registrationRepository.findByRaceId.mockResolvedValue([]); const result = await useCase.execute({ raceId: 'race-1' }); expect(result.isOk()).toBe(true); - const dto = result.unwrap(); - expect(dto.registeredDriverIds).toEqual([]); + const outputPort = result.unwrap(); + expect(outputPort.registrations).toEqual([]); }); }); \ No newline at end of file diff --git a/core/racing/application/use-cases/GetRaceRegistrationsUseCase.ts b/core/racing/application/use-cases/GetRaceRegistrationsUseCase.ts index 6c17140b6..0d4b6add5 100644 --- a/core/racing/application/use-cases/GetRaceRegistrationsUseCase.ts +++ b/core/racing/application/use-cases/GetRaceRegistrationsUseCase.ts @@ -1,6 +1,6 @@ import type { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository'; import type { GetRaceRegistrationsQueryParamsDTO } from '../dto/RaceRegistrationQueryDTO'; -import type { RaceRegistrationsResultDTO } from '../presenters/IRaceRegistrationsPresenter'; +import type { RaceRegistrationsOutputPort } from '../ports/output/RaceRegistrationsOutputPort'; import type { AsyncUseCase } from '@core/shared/application/AsyncUseCase'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; @@ -11,19 +11,19 @@ import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorC * Returns registered driver IDs for a race. * Orchestrates domain logic and delegates presentation to the presenter. */ -export class GetRaceRegistrationsUseCase implements AsyncUseCase { +export class GetRaceRegistrationsUseCase implements AsyncUseCase { constructor( private readonly registrationRepository: IRaceRegistrationRepository, ) {} - async execute(params: GetRaceRegistrationsQueryParamsDTO): Promise>> { + async execute(params: GetRaceRegistrationsQueryParamsDTO): Promise>> { const { raceId } = params; - const registeredDriverIds = await this.registrationRepository.getRegisteredDrivers(raceId); + const registrations = await this.registrationRepository.findByRaceId(raceId); - const dto: RaceRegistrationsResultDTO = { - registeredDriverIds, + const outputPort: RaceRegistrationsOutputPort = { + registrations, }; - return Result.ok(dto); + return Result.ok(outputPort); } } \ No newline at end of file diff --git a/core/racing/application/use-cases/GetRaceResultsDetailUseCase.ts b/core/racing/application/use-cases/GetRaceResultsDetailUseCase.ts index 39a769367..ae2012a25 100644 --- a/core/racing/application/use-cases/GetRaceResultsDetailUseCase.ts +++ b/core/racing/application/use-cases/GetRaceResultsDetailUseCase.ts @@ -3,7 +3,7 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit import type { IResultRepository } from '../../domain/repositories/IResultRepository'; import type { IDriverRepository } from '../../domain/repositories/IDriverRepository'; import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository'; -import type { RaceResultsDetailViewModel, RaceResultsPenaltySummaryViewModel } from '../presenters/IRaceResultsDetailPresenter'; +import type { RaceResultsDetailOutputPort } from '../ports/output/RaceResultsDetailOutputPort'; import type { AsyncUseCase } from '@core/shared/application/AsyncUseCase'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; @@ -19,7 +19,7 @@ export interface GetRaceResultsDetailParams { type GetRaceResultsDetailErrorCode = 'RACE_NOT_FOUND'; -export class GetRaceResultsDetailUseCase implements AsyncUseCase { +export class GetRaceResultsDetailUseCase implements AsyncUseCase { constructor( private readonly raceRepository: IRaceRepository, private readonly leagueRepository: ILeagueRepository, @@ -28,7 +28,7 @@ export class GetRaceResultsDetailUseCase implements AsyncUseCase>> { + async execute(params: GetRaceResultsDetailParams): Promise>> { const { raceId, driverId } = params; const race = await this.raceRepository.findById(raceId); @@ -49,31 +49,19 @@ export class GetRaceResultsDetailUseCase implements AsyncUseCase | undefined { @@ -129,11 +117,4 @@ export class GetRaceResultsDetailUseCase implements AsyncUseCase r.fastestLap)); } - private mapPenaltySummary(penalties: Penalty[]): RaceResultsPenaltySummaryViewModel[] { - return penalties.map((p) => ({ - driverId: p.driverId, - type: p.type, - ...(p.value !== undefined ? { value: p.value } : {}), - })); - } } \ No newline at end of file diff --git a/core/racing/application/use-cases/GetRaceWithSOFUseCase.ts b/core/racing/application/use-cases/GetRaceWithSOFUseCase.ts index 90c8af741..b0e8f4e2f 100644 --- a/core/racing/application/use-cases/GetRaceWithSOFUseCase.ts +++ b/core/racing/application/use-cases/GetRaceWithSOFUseCase.ts @@ -13,6 +13,7 @@ import type { IRaceRegistrationRepository } from '../../domain/repositories/IRac import type { IResultRepository } from '../../domain/repositories/IResultRepository'; import type { GetDriverRatingInputPort } from '../ports/input/GetDriverRatingInputPort'; import type { GetDriverRatingOutputPort } from '../ports/output/GetDriverRatingOutputPort'; +import type { RaceWithSOFOutputPort } from '../ports/output/RaceWithSOFOutputPort'; import { AverageStrengthOfFieldCalculator, type StrengthOfFieldCalculator, @@ -22,25 +23,9 @@ export interface GetRaceWithSOFQueryParams { raceId: string; } -export interface RaceWithSOFResultDTO { - raceId: string; - leagueId: string; - scheduledAt: Date; - track: string; - trackId: string; - car: string; - carId: string; - sessionType: string; - status: string; - strengthOfField: number | null; - registeredCount: number; - maxParticipants: number; - participantCount: number; -} - type GetRaceWithSOFErrorCode = 'RACE_NOT_FOUND'; -export class GetRaceWithSOFUseCase implements AsyncUseCase { +export class GetRaceWithSOFUseCase implements AsyncUseCase { private readonly sofCalculator: StrengthOfFieldCalculator; constructor( @@ -53,7 +38,7 @@ export class GetRaceWithSOFUseCase implements AsyncUseCase>> { + async execute(params: GetRaceWithSOFQueryParams): Promise>> { const { raceId } = params; const race = await this.raceRepository.findById(raceId); @@ -93,15 +78,12 @@ export class GetRaceWithSOFUseCase implements AsyncUseCase { +export class GetRacesPageDataUseCase implements AsyncUseCase { constructor( private readonly raceRepository: IRaceRepository, private readonly leagueRepository: ILeagueRepository, ) {} - async execute(): Promise>> { + async execute(): Promise>> { const [allRaces, allLeagues] = await Promise.all([ this.raceRepository.findAll(), this.leagueRepository.findAll(), @@ -25,20 +25,19 @@ export class GetRacesPageDataUseCase implements AsyncUseCase>> { + ): Promise>> { try { const { sponsorId } = params; @@ -146,7 +146,7 @@ export class GetSponsorDashboardUseCase { ? Math.min(100, (mainSponsorships * 30) + (sponsorships.length * 10)) : 0; - const dto: SponsorDashboardDTO = { + const outputPort: SponsorDashboardOutputPort = { sponsorId, sponsorName: sponsor.name, metrics: { @@ -167,7 +167,7 @@ export class GetSponsorDashboardUseCase { }, }; - return Result.ok(dto); + return Result.ok(outputPort); } catch { return Result.err({ code: 'REPOSITORY_ERROR', message: 'Failed to fetch sponsor dashboard' }); } diff --git a/core/racing/application/use-cases/GetSponsorSponsorshipsUseCase.ts b/core/racing/application/use-cases/GetSponsorSponsorshipsUseCase.ts index b489ff9c9..06dcb5407 100644 --- a/core/racing/application/use-cases/GetSponsorSponsorshipsUseCase.ts +++ b/core/racing/application/use-cases/GetSponsorSponsorshipsUseCase.ts @@ -11,7 +11,7 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository'; import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; import type { SponsorshipTier, SponsorshipStatus } from '../../domain/entities/SeasonSponsorship'; -import type { SponsorSponsorshipsViewModel } from '../presenters/ISponsorSponsorshipsPresenter'; +import type { SponsorSponsorshipsOutputPort } from '../ports/output/SponsorSponsorshipsOutputPort'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; @@ -76,7 +76,7 @@ export class GetSponsorSponsorshipsUseCase { async execute( params: GetSponsorSponsorshipsQueryParams, - ): Promise>> { + ): Promise>> { try { const { sponsorId } = params; @@ -153,7 +153,7 @@ export class GetSponsorSponsorshipsUseCase { const activeSponsorships = sponsorships.filter(s => s.status === 'active').length; - const dto: SponsorSponsorshipsDTO = { + const outputPort: SponsorSponsorshipsOutputPort = { sponsorId, sponsorName: sponsor.name, sponsorships: sponsorshipDetails, @@ -166,7 +166,7 @@ export class GetSponsorSponsorshipsUseCase { }, }; - return Result.ok(dto); + return Result.ok(outputPort); } catch { return Result.err({ code: 'REPOSITORY_ERROR', message: 'Failed to fetch sponsor sponsorships' }); } diff --git a/core/racing/application/use-cases/GetSponsorsUseCase.ts b/core/racing/application/use-cases/GetSponsorsUseCase.ts index 391821a2c..e5ffc9080 100644 --- a/core/racing/application/use-cases/GetSponsorsUseCase.ts +++ b/core/racing/application/use-cases/GetSponsorsUseCase.ts @@ -5,7 +5,7 @@ */ import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository'; -import type { GetSponsorsViewModel } from '../presenters/IGetSponsorsPresenter'; +import type { GetSponsorsOutputPort } from '../ports/output/GetSponsorsOutputPort'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; @@ -14,11 +14,11 @@ export class GetSponsorsUseCase { private readonly sponsorRepository: ISponsorRepository, ) {} - async execute(): Promise>> { + async execute(): Promise>> { try { const sponsors = await this.sponsorRepository.findAll(); - const viewModel: GetSponsorsViewModel = { + const outputPort: GetSponsorsOutputPort = { sponsors: sponsors.map(sponsor => ({ id: sponsor.id, name: sponsor.name, @@ -29,7 +29,7 @@ export class GetSponsorsUseCase { })), }; - return Result.ok(viewModel); + return Result.ok(outputPort); } catch { return Result.err({ code: 'REPOSITORY_ERROR', message: 'Failed to fetch sponsors' }); } diff --git a/core/racing/application/use-cases/GetSponsorshipPricingUseCase.ts b/core/racing/application/use-cases/GetSponsorshipPricingUseCase.ts index 69d3577a4..54ca55d8d 100644 --- a/core/racing/application/use-cases/GetSponsorshipPricingUseCase.ts +++ b/core/racing/application/use-cases/GetSponsorshipPricingUseCase.ts @@ -4,15 +4,17 @@ * Retrieves general sponsorship pricing tiers. */ -import type { GetSponsorshipPricingViewModel } from '../presenters/IGetSponsorshipPricingPresenter'; +import type { GetSponsorshipPricingOutputPort } from '../ports/output/GetSponsorshipPricingOutputPort'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; export class GetSponsorshipPricingUseCase { constructor() {} - async execute(): Promise>> { - const viewModel: GetSponsorshipPricingViewModel = { + async execute(): Promise>> { + const outputPort: GetSponsorshipPricingOutputPort = { + entityType: 'season', + entityId: '', pricing: [ { id: 'tier-bronze', level: 'Bronze', price: 100, currency: 'USD' }, { id: 'tier-silver', level: 'Silver', price: 250, currency: 'USD' }, @@ -20,6 +22,6 @@ export class GetSponsorshipPricingUseCase { ], }; - return Result.ok(viewModel); + return Result.ok(outputPort); } } \ No newline at end of file diff --git a/core/racing/application/use-cases/GetTeamDetailsUseCase.ts b/core/racing/application/use-cases/GetTeamDetailsUseCase.ts index c90e84d3f..bb87567fc 100644 --- a/core/racing/application/use-cases/GetTeamDetailsUseCase.ts +++ b/core/racing/application/use-cases/GetTeamDetailsUseCase.ts @@ -1,6 +1,6 @@ import type { ITeamRepository } from '../../domain/repositories/ITeamRepository'; import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository'; -import type { TeamDetailsViewModel } from '../presenters/ITeamDetailsPresenter'; +import type { GetTeamDetailsOutputPort } from '../ports/output/GetTeamDetailsOutputPort'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; @@ -15,7 +15,7 @@ export class GetTeamDetailsUseCase { async execute( params: { teamId: string; driverId: string }, - ): Promise>> { + ): Promise>> { try { const { teamId, driverId } = params; const team = await this.teamRepository.findById(teamId); @@ -25,25 +25,25 @@ export class GetTeamDetailsUseCase { const membership = await this.membershipRepository.getMembership(teamId, driverId); - const viewModel: TeamDetailsViewModel = { + const outputPort: GetTeamDetailsOutputPort = { team: { id: team.id, - name: team.name, - tag: team.tag, - description: team.description, + name: team.name.value, + tag: team.tag?.value ?? '', + description: team.description?.value ?? '', ownerId: team.ownerId, - leagues: team.leagues, - createdAt: team.createdAt.toISOString(), + leagues: team.leagues.map(l => l), // assuming leagues are strings + createdAt: team.createdAt instanceof Date ? team.createdAt : new Date(team.createdAt.value), }, membership: membership ? { role: membership.role as 'owner' | 'manager' | 'member', - joinedAt: membership.joinedAt.toISOString(), + joinedAt: membership.joinedAt instanceof Date ? membership.joinedAt : new Date(membership.joinedAt), isActive: membership.status === 'active', } : null, canManage: membership ? membership.role === 'owner' || membership.role === 'manager' : false, }; - return Result.ok(viewModel); + return Result.ok(outputPort); } catch { return Result.err({ code: 'REPOSITORY_ERROR', message: 'Failed to fetch team details' }); } diff --git a/core/racing/application/use-cases/GetTeamJoinRequestsUseCase.ts b/core/racing/application/use-cases/GetTeamJoinRequestsUseCase.ts index 30cbca1ba..c53baba4c 100644 --- a/core/racing/application/use-cases/GetTeamJoinRequestsUseCase.ts +++ b/core/racing/application/use-cases/GetTeamJoinRequestsUseCase.ts @@ -2,7 +2,7 @@ import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamM import type { IDriverRepository } from '../../domain/repositories/IDriverRepository'; import type { GetDriverAvatarInputPort } from '../ports/input/GetDriverAvatarInputPort'; import type { GetDriverAvatarOutputPort } from '../ports/output/GetDriverAvatarOutputPort'; -import type { TeamJoinRequestsResultDTO } from '../presenters/ITeamJoinRequestsPresenter'; +import type { TeamJoinRequestsOutputPort } from '../ports/output/TeamJoinRequestsOutputPort'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { AsyncUseCase } from '@core/shared/application'; @@ -11,7 +11,7 @@ import type { Logger } from '@core/shared/application'; /** * Use Case for retrieving team join requests. */ -export class GetTeamJoinRequestsUseCase implements AsyncUseCase<{ teamId: string }, TeamJoinRequestsResultDTO, 'REPOSITORY_ERROR'> +export class GetTeamJoinRequestsUseCase implements AsyncUseCase<{ teamId: string }, TeamJoinRequestsOutputPort, 'REPOSITORY_ERROR'> { constructor( private readonly membershipRepository: ITeamMembershipRepository, @@ -20,7 +20,7 @@ export class GetTeamJoinRequestsUseCase implements AsyncUseCase<{ teamId: string private readonly logger: Logger, ) {} - async execute(input: { teamId: string }): Promise>> { + async execute(input: { teamId: string }): Promise>> { this.logger.debug('Executing GetTeamJoinRequestsUseCase', { teamId: input.teamId }); try { @@ -33,7 +33,7 @@ export class GetTeamJoinRequestsUseCase implements AsyncUseCase<{ teamId: string for (const request of requests) { const driver = await this.driverRepository.findById(request.driverId); if (driver) { - driverNames[request.driverId] = driver.name; + driverNames[request.driverId] = driver.name.value; } else { this.logger.warn(`Driver not found for ID: ${request.driverId} during join request processing.`); } @@ -43,13 +43,23 @@ export class GetTeamJoinRequestsUseCase implements AsyncUseCase<{ teamId: string this.logger.debug('Processed driver details for join request', { driverId: request.driverId }); } - const dto: TeamJoinRequestsResultDTO = { - requests, - driverNames, - avatarUrls, + const requestsViewModel = requests.map(request => ({ + requestId: request.id, + driverId: request.driverId, + driverName: driverNames[request.driverId] || 'Unknown', + teamId: request.teamId, + status: request.status as 'pending' | 'approved' | 'rejected', + requestedAt: request.requestedAt instanceof Date ? request.requestedAt : new Date(request.requestedAt), + avatarUrl: avatarUrls[request.driverId] || '', + })); + + const outputPort: TeamJoinRequestsOutputPort = { + requests: requestsViewModel, + pendingCount: requests.filter(r => r.status === 'pending').length, + totalCount: requests.length, }; - return Result.ok(dto); + return Result.ok(outputPort); } catch (error) { this.logger.error('Error retrieving team join requests', { teamId: input.teamId, err: error }); return Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'Failed to retrieve team join requests' } }); diff --git a/core/racing/application/use-cases/GetTeamMembersUseCase.ts b/core/racing/application/use-cases/GetTeamMembersUseCase.ts index 5815968f3..6ea911c58 100644 --- a/core/racing/application/use-cases/GetTeamMembersUseCase.ts +++ b/core/racing/application/use-cases/GetTeamMembersUseCase.ts @@ -2,7 +2,7 @@ import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamM import type { IDriverRepository } from '../../domain/repositories/IDriverRepository'; import type { GetDriverAvatarInputPort } from '../ports/input/GetDriverAvatarInputPort'; import type { GetDriverAvatarOutputPort } from '../ports/output/GetDriverAvatarOutputPort'; -import type { TeamMembersResultDTO } from '../presenters/ITeamMembersPresenter'; +import type { TeamMembersOutputPort } from '../ports/output/TeamMembersOutputPort'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { AsyncUseCase } from '@core/shared/application'; @@ -11,7 +11,7 @@ import type { Logger } from '@core/shared/application'; /** * Use Case for retrieving team members. */ -export class GetTeamMembersUseCase implements AsyncUseCase<{ teamId: string }, TeamMembersResultDTO, 'REPOSITORY_ERROR'> +export class GetTeamMembersUseCase implements AsyncUseCase<{ teamId: string }, TeamMembersOutputPort, 'REPOSITORY_ERROR'> { constructor( private readonly membershipRepository: ITeamMembershipRepository, @@ -20,7 +20,7 @@ export class GetTeamMembersUseCase implements AsyncUseCase<{ teamId: string }, T private readonly logger: Logger, ) {} - async execute(input: { teamId: string }): Promise>> { + async execute(input: { teamId: string }): Promise>> { this.logger.debug(`Executing GetTeamMembersUseCase for teamId: ${input.teamId}`); try { @@ -34,7 +34,7 @@ export class GetTeamMembersUseCase implements AsyncUseCase<{ teamId: string }, T this.logger.debug(`Processing membership for driverId: ${membership.driverId}`); const driver = await this.driverRepository.findById(membership.driverId); if (driver) { - driverNames[membership.driverId] = driver.name; + driverNames[membership.driverId] = driver.name.value; } else { this.logger.warn(`Driver with ID ${membership.driverId} not found while fetching team members for team ${input.teamId}.`); } @@ -43,13 +43,24 @@ export class GetTeamMembersUseCase implements AsyncUseCase<{ teamId: string }, T avatarUrls[membership.driverId] = avatarResult.avatarUrl; } - const dto: TeamMembersResultDTO = { - memberships, - driverNames, - avatarUrls, + const members = memberships.map(membership => ({ + driverId: membership.driverId, + driverName: driverNames[membership.driverId] || 'Unknown', + role: membership.role as 'owner' | 'manager' | 'member', + joinedAt: membership.joinedAt instanceof Date ? membership.joinedAt : new Date(membership.joinedAt), + isActive: membership.status === 'active', + avatarUrl: avatarUrls[membership.driverId] || '', + })); + + const outputPort: TeamMembersOutputPort = { + members, + totalCount: memberships.length, + ownerCount: memberships.filter(m => m.role === 'owner').length, + managerCount: memberships.filter(m => m.role === 'manager').length, + memberCount: memberships.filter(m => m.role === 'member').length, }; - return Result.ok(dto); + return Result.ok(outputPort); } catch (error) { this.logger.error(`Error in GetTeamMembersUseCase for teamId: ${input.teamId}`, error as Error, { teamId: input.teamId }); return Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'Failed to retrieve team members' } }); diff --git a/core/racing/application/use-cases/GetTeamsLeaderboardUseCase.ts b/core/racing/application/use-cases/GetTeamsLeaderboardUseCase.ts index cfc22bed6..97b8a1a10 100644 --- a/core/racing/application/use-cases/GetTeamsLeaderboardUseCase.ts +++ b/core/racing/application/use-cases/GetTeamsLeaderboardUseCase.ts @@ -1,7 +1,7 @@ import type { ITeamRepository } from '@core/racing/domain/repositories/ITeamRepository'; import type { ITeamMembershipRepository } from '@core/racing/domain/repositories/ITeamMembershipRepository'; import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository'; -import type { TeamsLeaderboardResultDTO } from '@core/racing/application/presenters/ITeamsLeaderboardPresenter'; +import type { TeamsLeaderboardOutputPort, SkillLevel } from '../ports/output/TeamsLeaderboardOutputPort'; import { SkillLevelService } from '@core/racing/domain/services/SkillLevelService'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; @@ -30,7 +30,7 @@ interface TeamLeaderboardItem { /** * Use case: GetTeamsLeaderboardUseCase */ -export class GetTeamsLeaderboardUseCase implements AsyncUseCase +export class GetTeamsLeaderboardUseCase implements AsyncUseCase { constructor( private readonly teamRepository: ITeamRepository, @@ -40,7 +40,7 @@ export class GetTeamsLeaderboardUseCase implements AsyncUseCase>> { + async execute(): Promise>> { try { const allTeams = await this.teamRepository.findAll(); const teams: TeamLeaderboardItem[] = []; @@ -88,12 +88,28 @@ export class GetTeamsLeaderboardUseCase implements AsyncUseCase t.isRecruiting).length; - const result: TeamsLeaderboardResultDTO = { - teams, - recruitingCount, + const groupsBySkillLevel: Record = { + beginner: [], + intermediate: [], + advanced: [], + pro: [], }; - return Result.ok(result); + teams.forEach(team => { + const level = team.performanceLevel as SkillLevel; + groupsBySkillLevel[level].push(team); + }); + + const topTeams = teams.slice(0, 10); // Assuming top 10 + + const outputPort: TeamsLeaderboardOutputPort = { + teams, + recruitingCount, + groupsBySkillLevel, + topTeams, + }; + + return Result.ok(outputPort); } catch (error) { this.logger.error('Error retrieving teams leaderboard', error as Error); return Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'Failed to retrieve teams leaderboard' } }); diff --git a/core/racing/application/use-cases/GetTotalDriversUseCase.ts b/core/racing/application/use-cases/GetTotalDriversUseCase.ts index b00d34f15..7245317fb 100644 --- a/core/racing/application/use-cases/GetTotalDriversUseCase.ts +++ b/core/racing/application/use-cases/GetTotalDriversUseCase.ts @@ -1,5 +1,5 @@ import type { IDriverRepository } from '../../domain/repositories/IDriverRepository'; -import type { TotalDriversResultDTO } from '../presenters/ITotalDriversPresenter'; +import type { TotalDriversOutputPort } from '../ports/output/TotalDriversOutputPort'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { AsyncUseCase } from '@core/shared/application'; @@ -8,21 +8,21 @@ import type { Logger } from '@core/shared/application'; /** * Use Case for retrieving total number of drivers. */ -export class GetTotalDriversUseCase implements AsyncUseCase +export class GetTotalDriversUseCase implements AsyncUseCase { constructor( private readonly driverRepository: IDriverRepository, private readonly logger: Logger, ) {} - async execute(): Promise>> { + async execute(): Promise>> { try { const drivers = await this.driverRepository.findAll(); - const dto: TotalDriversResultDTO = { + const output: TotalDriversOutputPort = { totalDrivers: drivers.length, }; - return Result.ok(dto); + return Result.ok(output); } catch (error) { this.logger.error('Error retrieving total drivers', error as Error); return Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'Failed to retrieve total drivers' } }); diff --git a/core/racing/application/use-cases/GetTotalLeaguesUseCase.ts b/core/racing/application/use-cases/GetTotalLeaguesUseCase.ts index 7602370a2..d384f5dcd 100644 --- a/core/racing/application/use-cases/GetTotalLeaguesUseCase.ts +++ b/core/racing/application/use-cases/GetTotalLeaguesUseCase.ts @@ -1,23 +1,23 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository'; -import type { GetTotalLeaguesResultDTO } from '../presenters/IGetTotalLeaguesPresenter'; +import type { GetTotalLeaguesOutputPort } from '../ports/output/GetTotalLeaguesOutputPort'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { AsyncUseCase } from '@core/shared/application'; import type { Logger } from '@core/shared/application'; -export class GetTotalLeaguesUseCase implements AsyncUseCase +export class GetTotalLeaguesUseCase implements AsyncUseCase { constructor( private readonly leagueRepository: ILeagueRepository, private readonly logger: Logger, ) {} - async execute(): Promise>> { + async execute(): Promise>> { try { const leagues = await this.leagueRepository.findAll(); - const dto: GetTotalLeaguesResultDTO = { totalLeagues: leagues.length }; + const output: GetTotalLeaguesOutputPort = { totalLeagues: leagues.length }; - return Result.ok(dto); + return Result.ok(output); } catch (error) { this.logger.error('Error retrieving total leagues', error as Error); return Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'Failed to retrieve total leagues' } }); diff --git a/core/racing/application/use-cases/GetTotalRacesUseCase.ts b/core/racing/application/use-cases/GetTotalRacesUseCase.ts index 7de365bab..f27d659b5 100644 --- a/core/racing/application/use-cases/GetTotalRacesUseCase.ts +++ b/core/racing/application/use-cases/GetTotalRacesUseCase.ts @@ -1,23 +1,23 @@ import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; -import type { GetTotalRacesResultDTO } from '../presenters/IGetTotalRacesPresenter'; +import type { GetTotalRacesOutputPort } from '../ports/output/GetTotalRacesOutputPort'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { AsyncUseCase } from '@core/shared/application'; import type { Logger } from '@core/shared/application'; -export class GetTotalRacesUseCase implements AsyncUseCase +export class GetTotalRacesUseCase implements AsyncUseCase { constructor( private readonly raceRepository: IRaceRepository, private readonly logger: Logger, ) {} - async execute(): Promise>> { + async execute(): Promise>> { try { const races = await this.raceRepository.findAll(); - const dto: GetTotalRacesResultDTO = { totalRaces: races.length }; + const output: GetTotalRacesOutputPort = { totalRaces: races.length }; - return Result.ok(dto); + return Result.ok(output); } catch (error) { this.logger.error('Error retrieving total races', error as Error); return Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'Failed to retrieve total races' } }); diff --git a/core/racing/application/use-cases/ImportRaceResultsApiUseCase.ts b/core/racing/application/use-cases/ImportRaceResultsApiUseCase.ts index 037f474a1..433ddbbbe 100644 --- a/core/racing/application/use-cases/ImportRaceResultsApiUseCase.ts +++ b/core/racing/application/use-cases/ImportRaceResultsApiUseCase.ts @@ -7,7 +7,7 @@ import { Result } from '../../domain/entities/Result'; import type { AsyncUseCase, Logger } from '@core/shared/application'; import { Result as SharedResult } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; -import type { ImportRaceResultsApiResultDTO } from '../presenters/IImportRaceResultsApiPresenter'; +import type { ImportRaceResultsApiOutputPort } from '../ports/output/ImportRaceResultsApiOutputPort'; export interface ImportRaceResultDTO { id: string; @@ -29,7 +29,7 @@ type ImportRaceResultsApiErrorCode = type ImportRaceResultsApiApplicationError = ApplicationErrorCode; -export class ImportRaceResultsApiUseCase implements AsyncUseCase<{ raceId: string; resultsFileContent: string }, ImportRaceResultsApiResultDTO, ImportRaceResultsApiErrorCode> { +export class ImportRaceResultsApiUseCase implements AsyncUseCase<{ raceId: string; resultsFileContent: string }, ImportRaceResultsApiOutputPort, ImportRaceResultsApiErrorCode> { constructor( private readonly raceRepository: IRaceRepository, private readonly leagueRepository: ILeagueRepository, @@ -39,7 +39,7 @@ export class ImportRaceResultsApiUseCase implements AsyncUseCase<{ raceId: strin private readonly logger: Logger, ) {} - async execute(params: { raceId: string; resultsFileContent: string }): Promise>> { + async execute(params: { raceId: string; resultsFileContent: string }): Promise>> { this.logger.debug('ImportRaceResultsApiUseCase:execute', { raceId: params.raceId }); const { raceId, resultsFileContent } = params; @@ -107,9 +107,10 @@ export class ImportRaceResultsApiUseCase implements AsyncUseCase<{ raceId: strin await this.standingRepository.recalculate(league.id); this.logger.info('ImportRaceResultsApiUseCase:standings recalculated', { leagueId: league.id }); - const dto: ImportRaceResultsApiResultDTO = { + const output: ImportRaceResultsApiOutputPort = { success: true, raceId, + leagueId: league.id, driversProcessed: results.length, resultsRecorded: validEntities.length, errors: [], diff --git a/core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase.ts b/core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase.ts index ce9a0b2d7..db1cd132e 100644 --- a/core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase.ts +++ b/core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase.ts @@ -3,6 +3,7 @@ import type { IsDriverRegisteredForRaceQueryParamsDTO } from '../dto/RaceRegistr import type { AsyncUseCase, Logger } from '@core/shared/application'; import { Result as SharedResult } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; +import type { DriverRegistrationStatusOutputPort } from '../ports/output/DriverRegistrationStatusOutputPort'; type IsDriverRegisteredForRaceErrorCode = 'REPOSITORY_ERROR'; @@ -14,20 +15,20 @@ type IsDriverRegisteredForRaceApplicationError = ApplicationErrorCode + implements AsyncUseCase { constructor( private readonly registrationRepository: IRaceRegistrationRepository, private readonly logger: Logger, ) {} - async execute(params: IsDriverRegisteredForRaceQueryParamsDTO): Promise> { + async execute(params: IsDriverRegisteredForRaceQueryParamsDTO): Promise> { this.logger.debug('IsDriverRegisteredForRaceUseCase:execute', { params }); const { raceId, driverId } = params; try { const isRegistered = await this.registrationRepository.isRegistered(raceId, driverId); - return SharedResult.ok(isRegistered); + return SharedResult.ok({ isRegistered, raceId, driverId }); } catch (error) { this.logger.error('IsDriverRegisteredForRaceUseCase:execution error', error instanceof Error ? error : new Error('Unknown error')); return SharedResult.err({ code: 'REPOSITORY_ERROR', details: { message: error instanceof Error ? error.message : 'Unknown error' } }); diff --git a/core/racing/application/use-cases/JoinLeagueUseCase.test.ts b/core/racing/application/use-cases/JoinLeagueUseCase.test.ts index c59d72692..6815f5ac5 100644 --- a/core/racing/application/use-cases/JoinLeagueUseCase.test.ts +++ b/core/racing/application/use-cases/JoinLeagueUseCase.test.ts @@ -50,12 +50,9 @@ describe('JoinLeagueUseCase', () => { expect(result.isOk()).toBe(true); expect(result.unwrap()).toEqual({ - id: 'membership-1', + membershipId: 'membership-1', leagueId: 'league-1', - driverId: 'driver-1', - role: 'member', status: 'active', - joinedAt: expect.any(Date), }); }); @@ -76,7 +73,6 @@ describe('JoinLeagueUseCase', () => { expect(result.isErr()).toBe(true); expect(result.unwrapErr()).toEqual({ code: 'ALREADY_MEMBER', - details: { message: 'Already a member or have a pending request' }, }); }); @@ -91,7 +87,6 @@ describe('JoinLeagueUseCase', () => { expect(result.isErr()).toBe(true); expect(result.unwrapErr()).toEqual({ code: 'REPOSITORY_ERROR', - details: { message: 'Repository error' }, }); }); }); \ No newline at end of file diff --git a/core/racing/application/use-cases/JoinLeagueUseCase.ts b/core/racing/application/use-cases/JoinLeagueUseCase.ts index fb1db6c26..7e63a1367 100644 --- a/core/racing/application/use-cases/JoinLeagueUseCase.ts +++ b/core/racing/application/use-cases/JoinLeagueUseCase.ts @@ -4,14 +4,13 @@ import { LeagueMembership, type MembershipRole, type MembershipStatus } from '.. import type { AsyncUseCase } from '@core/shared/application'; import { Result as SharedResult } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; +import type { JoinLeagueOutputPort } from '../ports/output/JoinLeagueOutputPort'; import type { JoinLeagueCommandDTO } from '../dto/JoinLeagueCommandDTO'; type JoinLeagueErrorCode = 'ALREADY_MEMBER' | 'REPOSITORY_ERROR'; -type JoinLeagueApplicationError = ApplicationErrorCode; - -export class JoinLeagueUseCase implements AsyncUseCase { +export class JoinLeagueUseCase implements AsyncUseCase { constructor( private readonly membershipRepository: ILeagueMembershipRepository, private readonly logger: Logger, @@ -24,7 +23,7 @@ export class JoinLeagueUseCase implements AsyncUseCase> { + async execute(command: JoinLeagueCommandDTO): Promise>> { this.logger.debug('Attempting to join league', { command }); const { leagueId, driverId } = command; @@ -32,7 +31,7 @@ export class JoinLeagueUseCase implements AsyncUseCase { let useCase: ListLeagueScoringPresetsUseCase; - let presetProvider: { - listPresets: Mock; - }; beforeEach(() => { - presetProvider = { - listPresets: vi.fn(), - }; - useCase = new ListLeagueScoringPresetsUseCase( - presetProvider as unknown as LeagueScoringPresetProvider, - ); + const mockPresets = [ + { + id: 'preset-1', + name: 'Preset 1', + description: 'Desc 1', + primaryChampionshipType: 'driver' as const, + dropPolicySummary: 'Drop 1', + sessionSummary: 'Session 1', + bonusSummary: 'Bonus 1', + createConfig: vi.fn(), + }, + { + id: 'preset-2', + name: 'Preset 2', + description: 'Desc 2', + primaryChampionshipType: 'team' as const, + dropPolicySummary: 'Drop 2', + sessionSummary: 'Session 2', + bonusSummary: 'Bonus 2', + createConfig: vi.fn(), + }, + ]; + useCase = new ListLeagueScoringPresetsUseCase(mockPresets); }); it('should list presets successfully', async () => { - const mockPresets = [ - { id: 'preset-1', name: 'Preset 1' }, - { id: 'preset-2', name: 'Preset 2' }, - ]; - - presetProvider.listPresets.mockResolvedValue(mockPresets); - const result = await useCase.execute(); expect(result.isOk()).toBe(true); expect(result.unwrap()).toEqual({ - presets: mockPresets, + presets: [ + { + id: 'preset-1', + name: 'Preset 1', + description: 'Desc 1', + primaryChampionshipType: 'driver', + sessionSummary: 'Session 1', + bonusSummary: 'Bonus 1', + dropPolicySummary: 'Drop 1', + }, + { + id: 'preset-2', + name: 'Preset 2', + description: 'Desc 2', + primaryChampionshipType: 'team', + sessionSummary: 'Session 2', + bonusSummary: 'Bonus 2', + dropPolicySummary: 'Drop 2', + }, + ], }); }); }); \ No newline at end of file diff --git a/core/racing/application/use-cases/ListLeagueScoringPresetsUseCase.ts b/core/racing/application/use-cases/ListLeagueScoringPresetsUseCase.ts index 0048bec24..bf5b410ee 100644 --- a/core/racing/application/use-cases/ListLeagueScoringPresetsUseCase.ts +++ b/core/racing/application/use-cases/ListLeagueScoringPresetsUseCase.ts @@ -1,5 +1,6 @@ import type { LeagueScoringPresetOutputPort } from '../ports/output/LeagueScoringPresetOutputPort'; -import type { LeagueScoringPresetsResultDTO } from '../presenters/ILeagueScoringPresetsPresenter'; +import type { LeagueScoringPresetsOutputPort } from '../ports/output/LeagueScoringPresetsOutputPort'; +import type { LeagueScoringPreset } from '../../../bootstrap/LeagueScoringPresets'; import type { AsyncUseCase } from '@core/shared/application/AsyncUseCase'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; @@ -9,15 +10,23 @@ import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorC * Returns preset data without business logic. */ export class ListLeagueScoringPresetsUseCase - implements AsyncUseCase + implements AsyncUseCase { - constructor(private readonly presets: LeagueScoringPresetOutputPort[]) {} + constructor(private readonly presets: LeagueScoringPreset[]) {} - async execute(): Promise>> { - const dto: LeagueScoringPresetsResultDTO = { - presets: this.presets, + async execute(): Promise>> { + const output: LeagueScoringPresetsOutputPort = { + presets: this.presets.map(p => ({ + id: p.id, + name: p.name, + description: p.description, + primaryChampionshipType: p.primaryChampionshipType, + sessionSummary: p.sessionSummary, + bonusSummary: p.bonusSummary, + dropPolicySummary: p.dropPolicySummary, + } as LeagueScoringPresetOutputPort)), }; - return Result.ok(dto); + return Result.ok(output); } } \ No newline at end of file diff --git a/core/racing/application/use-cases/PreviewLeagueScheduleUseCase.ts b/core/racing/application/use-cases/PreviewLeagueScheduleUseCase.ts index ead609ad8..096bfd8d2 100644 --- a/core/racing/application/use-cases/PreviewLeagueScheduleUseCase.ts +++ b/core/racing/application/use-cases/PreviewLeagueScheduleUseCase.ts @@ -1,6 +1,7 @@ import { SeasonScheduleGenerator } from '../../domain/services/SeasonScheduleGenerator'; -import type { LeagueSchedulePreviewDTO, LeagueScheduleDTO } from '../dto/LeagueScheduleDTO'; +import type { LeagueScheduleDTO } from '../dto/LeagueScheduleDTO'; import { scheduleDTOToSeasonSchedule } from '../dto/LeagueScheduleDTO'; +import type { LeagueSchedulePreviewOutputPort } from '../ports/output/LeagueSchedulePreviewOutputPort'; import type { AsyncUseCase } from '@core/shared/application/AsyncUseCase'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; @@ -16,13 +17,13 @@ type PreviewLeagueScheduleErrorCode = 'INVALID_SCHEDULE'; type PreviewLeagueScheduleApplicationError = ApplicationErrorCode; -export class PreviewLeagueScheduleUseCase implements AsyncUseCase { +export class PreviewLeagueScheduleUseCase implements AsyncUseCase { constructor( private readonly scheduleGenerator: typeof SeasonScheduleGenerator = SeasonScheduleGenerator, private readonly logger: Logger, ) {} - async execute(params: PreviewLeagueScheduleQueryParams): Promise> { + async execute(params: PreviewLeagueScheduleQueryParams): Promise> { this.logger.debug('Previewing league schedule', { params }); let seasonSchedule: SeasonSchedule; @@ -48,7 +49,7 @@ export class PreviewLeagueScheduleUseCase implements AsyncUseCase { +export class RejectLeagueJoinRequestUseCase implements AsyncUseCase { constructor(private readonly leagueMembershipRepository: ILeagueMembershipRepository) {} - async execute(params: RejectLeagueJoinRequestUseCaseParams): Promise>> { + async execute(params: RejectLeagueJoinRequestUseCaseParams): Promise>> { await this.leagueMembershipRepository.removeJoinRequest(params.requestId); - const dto: RejectLeagueJoinRequestResultDTO = { success: true, message: 'Join request rejected.' }; - return Result.ok(dto); + const port: RejectLeagueJoinRequestOutputPort = { success: true, message: 'Join request rejected.' }; + return Result.ok(port); } } \ No newline at end of file diff --git a/core/racing/application/use-cases/RemoveLeagueMemberUseCase.ts b/core/racing/application/use-cases/RemoveLeagueMemberUseCase.ts index 82b00fea4..349da44db 100644 --- a/core/racing/application/use-cases/RemoveLeagueMemberUseCase.ts +++ b/core/racing/application/use-cases/RemoveLeagueMemberUseCase.ts @@ -1,20 +1,17 @@ import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; +import type { RemoveLeagueMemberOutputPort } from '../ports/output/RemoveLeagueMemberOutputPort'; export interface RemoveLeagueMemberUseCaseParams { leagueId: string; targetDriverId: string; } -export interface RemoveLeagueMemberResultDTO { - success: boolean; -} - export class RemoveLeagueMemberUseCase { constructor(private readonly leagueMembershipRepository: ILeagueMembershipRepository) {} - async execute(params: RemoveLeagueMemberUseCaseParams): Promise>> { + async execute(params: RemoveLeagueMemberUseCaseParams): Promise>> { const memberships = await this.leagueMembershipRepository.getLeagueMembers(params.leagueId); const membership = memberships.find(m => m.driverId === params.targetDriverId); if (!membership) { diff --git a/core/racing/application/use-cases/TransferLeagueOwnershipUseCase.ts b/core/racing/application/use-cases/TransferLeagueOwnershipUseCase.ts index 67cdcc78c..a6b009028 100644 --- a/core/racing/application/use-cases/TransferLeagueOwnershipUseCase.ts +++ b/core/racing/application/use-cases/TransferLeagueOwnershipUseCase.ts @@ -7,7 +7,7 @@ import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeague import type { MembershipRole, } from '@core/racing/domain/entities/LeagueMembership'; -import type { TransferLeagueOwnershipResultDTO } from '../presenters/ITransferLeagueOwnershipPresenter'; +import type { TransferLeagueOwnershipOutputPort } from '../ports/output/TransferLeagueOwnershipOutputPort'; export interface TransferLeagueOwnershipCommandDTO { leagueId: string; @@ -23,7 +23,7 @@ export class TransferLeagueOwnershipUseCase { private readonly membershipRepository: ILeagueMembershipRepository ) {} - async execute(command: TransferLeagueOwnershipCommandDTO): Promise>> { + async execute(command: TransferLeagueOwnershipCommandDTO): Promise>> { const { leagueId, currentOwnerId, newOwnerId } = command; const league = await this.leagueRepository.findById(leagueId); @@ -57,6 +57,6 @@ export class TransferLeagueOwnershipUseCase { const updatedLeague = league.update({ ownerId: newOwnerId }); await this.leagueRepository.update(updatedLeague); - return Result.ok(undefined); + return Result.ok({ success: true }); } } \ No newline at end of file diff --git a/core/racing/application/use-cases/UpdateLeagueMemberRoleUseCase.ts b/core/racing/application/use-cases/UpdateLeagueMemberRoleUseCase.ts index 445e5f4c0..e8134f848 100644 --- a/core/racing/application/use-cases/UpdateLeagueMemberRoleUseCase.ts +++ b/core/racing/application/use-cases/UpdateLeagueMemberRoleUseCase.ts @@ -1,7 +1,7 @@ import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository'; -import type { UpdateLeagueMemberRoleResultDTO } from '../presenters/IUpdateLeagueMemberRolePresenter'; +import type { UpdateLeagueMemberRoleOutputPort } from '../ports/output/UpdateLeagueMemberRoleOutputPort'; export interface UpdateLeagueMemberRoleUseCaseParams { leagueId: string; @@ -12,7 +12,7 @@ export interface UpdateLeagueMemberRoleUseCaseParams { export class UpdateLeagueMemberRoleUseCase { constructor(private readonly leagueMembershipRepository: ILeagueMembershipRepository) {} - async execute(params: UpdateLeagueMemberRoleUseCaseParams): Promise>> { + async execute(params: UpdateLeagueMemberRoleUseCaseParams): Promise>> { const memberships = await this.leagueMembershipRepository.getLeagueMembers(params.leagueId); const membership = memberships.find(m => m.driverId === params.targetDriverId); if (!membership) { diff --git a/core/racing/domain/repositories/IRaceRegistrationRepository.ts b/core/racing/domain/repositories/IRaceRegistrationRepository.ts index 962561934..b767ec4d2 100644 --- a/core/racing/domain/repositories/IRaceRegistrationRepository.ts +++ b/core/racing/domain/repositories/IRaceRegistrationRepository.ts @@ -18,6 +18,11 @@ export interface IRaceRegistrationRepository { */ getRegisteredDrivers(raceId: string): Promise; + /** + * Get all registrations for a race. + */ + findByRaceId(raceId: string): Promise; + /** * Get the number of registrations for a race. */ diff --git a/core/racing/index.ts b/core/racing/index.ts index 039aaf461..b856a0cbc 100644 --- a/core/racing/index.ts +++ b/core/racing/index.ts @@ -45,10 +45,17 @@ export * from './infrastructure/repositories/InMemorySponsorshipPricingRepositor export * from './application/dtos/LeagueDriverSeasonStatsDTO'; export * from './application/dtos/LeagueScoringConfigDTO'; +export * from './application/ports/output/CreateLeagueWithSeasonAndScoringOutputPort'; +export * from './application/ports/output/DashboardOverviewOutputPort'; +export * from './application/ports/output/DriversLeaderboardOutputPort'; + +export * from './application/use-cases/CreateSponsorUseCase'; export * from './application/use-cases/GetSponsorDashboardUseCase'; export * from './application/use-cases/GetSponsorSponsorshipsUseCase'; export * from './application/use-cases/ApplyForSponsorshipUseCase'; export * from './application/use-cases/AcceptSponsorshipRequestUseCase'; export * from './application/use-cases/RejectSponsorshipRequestUseCase'; export * from './application/use-cases/GetPendingSponsorshipRequestsUseCase'; -export * from './application/use-cases/GetEntitySponsorshipPricingUseCase'; \ No newline at end of file +export * from './application/use-cases/GetEntitySponsorshipPricingUseCase'; + +export * from './application/ports/output/CreateSponsorOutputPort'; \ No newline at end of file diff --git a/tests/RegistrationAndTeamUseCases.test.ts b/tests/RegistrationAndTeamUseCases.test.ts index 23ae01658..75bc81237 100644 --- a/tests/RegistrationAndTeamUseCases.test.ts +++ b/tests/RegistrationAndTeamUseCases.test.ts @@ -1,4 +1,5 @@ import { describe, it, expect, beforeEach } from 'vitest'; +import type { Logger } from '@core/shared/application'; import type { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository'; import type { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository'; @@ -31,13 +32,8 @@ import { GetRaceRegistrationsUseCase } from '@core/racing/application/use-cases/ -import type { IDriverRegistrationStatusPresenter } from '@core/racing/application/presenters/IDriverRegistrationStatusPresenter'; import type { IRaceRegistrationsPresenter } from '@core/racing/application/presenters/IRaceRegistrationsPresenter'; -import type { - IAllTeamsPresenter, - AllTeamsResultDTO, - AllTeamsViewModel, -} from '@core/racing/application/presenters/IAllTeamsPresenter'; +import type { GetAllTeamsOutputPort } from '@core/racing/application/ports/output/GetAllTeamsOutputPort'; import type { ITeamDetailsPresenter } from '@core/racing/application/presenters/ITeamDetailsPresenter'; import type { ITeamMembersPresenter, @@ -162,30 +158,6 @@ class InMemoryLeagueMembershipRepositoryForRegistrations implements ILeagueMembe } } -class TestDriverRegistrationStatusPresenter implements IDriverRegistrationStatusPresenter { - isRegistered: boolean | null = null; - raceId: string | null = null; - driverId: string | null = null; - - present(isRegistered: boolean, raceId: string, driverId: string) { - this.isRegistered = isRegistered; - this.raceId = raceId; - this.driverId = driverId; - return { - isRegistered, - raceId, - driverId, - }; - } - - getViewModel() { - return { - isRegistered: this.isRegistered!, - raceId: this.raceId!, - driverId: this.driverId!, - }; - } -} class TestRaceRegistrationsPresenter implements IRaceRegistrationsPresenter { raceId: string | null = null; @@ -352,7 +324,7 @@ describe('Racing application use-cases - registrations', () => { let withdrawFromRace: WithdrawFromRaceUseCase; let isDriverRegistered: IsDriverRegisteredForRaceUseCase; let getRaceRegistrations: GetRaceRegistrationsUseCase; - let driverRegistrationPresenter: TestDriverRegistrationStatusPresenter; + let logger: Logger; let raceRegistrationsPresenter: TestRaceRegistrationsPresenter; beforeEach(() => { @@ -361,10 +333,10 @@ describe('Racing application use-cases - registrations', () => { registerForRace = new RegisterForRaceUseCase(registrationRepo, membershipRepo); withdrawFromRace = new WithdrawFromRaceUseCase(registrationRepo); - driverRegistrationPresenter = new TestDriverRegistrationStatusPresenter(); + logger = { debug: vi.fn(), error: vi.fn(), info: vi.fn(), warn: vi.fn() }; isDriverRegistered = new IsDriverRegisteredForRaceUseCase( registrationRepo, - driverRegistrationPresenter, + logger, ); raceRegistrationsPresenter = new TestRaceRegistrationsPresenter(); getRaceRegistrations = new GetRaceRegistrationsUseCase(registrationRepo); @@ -379,10 +351,12 @@ describe('Racing application use-cases - registrations', () => { await registerForRace.execute({ raceId, leagueId, driverId }); - await isDriverRegistered.execute({ raceId, driverId }); - expect(driverRegistrationPresenter.isRegistered).toBe(true); - expect(driverRegistrationPresenter.raceId).toBe(raceId); - expect(driverRegistrationPresenter.driverId).toBe(driverId); + const result = await isDriverRegistered.execute({ raceId, driverId }); + expect(result.isOk()).toBe(true); + const status = result.unwrap(); + expect(status.isRegistered).toBe(true); + expect(status.raceId).toBe(raceId); + expect(status.driverId).toBe(driverId); await getRaceRegistrations.execute({ raceId }, raceRegistrationsPresenter); expect(raceRegistrationsPresenter.driverIds).toContain(driverId); @@ -408,8 +382,10 @@ describe('Racing application use-cases - registrations', () => { await withdrawFromRace.execute({ raceId, driverId }); - await isDriverRegistered.execute({ raceId, driverId }); - expect(driverRegistrationPresenter.isRegistered).toBe(false); + const result = await isDriverRegistered.execute({ raceId, driverId }); + expect(result.isOk()).toBe(true); + const status = result.unwrap(); + expect(status.isRegistered).toBe(false); await getRaceRegistrations.execute({ raceId }, raceRegistrationsPresenter); expect(raceRegistrationsPresenter.driverIds).toEqual([]);