16 KiB
Clean Architecture Violation Fix Plan
Executive Summary
Problem: The codebase violates Clean Architecture by having use cases call presenters directly (this.output.present()), creating tight coupling and causing "Presenter not presented" errors.
Root Cause: Use cases are doing the presenter's job instead of returning data and letting controllers handle the wiring.
Solution: Remove ALL .present() calls from use cases. Use cases return Results. Controllers wire Results to Presenters.
The Violation Pattern
❌ Current Wrong Pattern (Violates Clean Architecture)
// core/racing/application/use-cases/GetRaceDetailUseCase.ts
class GetRaceDetailUseCase {
constructor(
private repositories: any,
private output: UseCaseOutputPort<GetRaceDetailResult> // ❌ Wrong
) {}
async execute(input: GetRaceDetailInput): Promise<Result<void, ApplicationError>> {
const race = await this.raceRepository.findById(input.raceId);
if (!race) {
const result = Result.err({ code: 'RACE_NOT_FOUND', details: {...} });
this.output.present(result); // ❌ WRONG: Use case calling presenter
return result;
}
const result = Result.ok({ race, league, registrations, drivers, userResult, isUserRegistered, canRegister });
this.output.present(result); // ❌ WRONG: Use case calling presenter
return result;
}
}
✅ Correct Pattern (Clean Architecture)
// core/racing/application/use-cases/GetRaceDetailUseCase.ts
class GetRaceDetailUseCase {
constructor(
private repositories: any,
// NO output port - removed
) {}
async execute(input: GetRaceDetailInput): Promise<Result<GetRaceDetailResult, ApplicationError>> {
const race = await this.raceRepository.findById(input.raceId);
if (!race) {
return Result.err({ code: 'RACE_NOT_FOUND', details: {...} });
// ✅ No .present() call
}
return Result.ok({ race, league, registrations, drivers, userResult, isUserRegistered, canRegister });
// ✅ No .present() call
}
}
// apps/api/src/domain/race/RaceService.ts (Controller layer)
class RaceService {
constructor(
private getRaceDetailUseCase: GetRaceDetailUseCase,
private raceDetailPresenter: RaceDetailPresenter,
) {}
async getRaceDetail(params: GetRaceDetailParamsDTO): Promise<RaceDetailPresenter> {
const result = await this.getRaceDetailUseCase.execute(params);
if (result.isErr()) {
throw new NotFoundException(result.error.details.message);
}
this.raceDetailPresenter.present(result.value); // ✅ Controller wires to presenter
return this.raceDetailPresenter;
}
}
What Needs To Be Done
Phase 1: Fix Use Cases (Remove Output Ports)
Files to modify in core/racing/application/use-cases/:
-
GetRaceDetailUseCase.ts (lines 35-44, 46-115)
- Remove
output: UseCaseOutputPort<GetRaceDetailResult>from constructor - Change return type from
Promise<Result<void, ApplicationError>>toPromise<Result<GetRaceDetailResult, ApplicationError>> - Remove all
this.output.present()calls (lines 100, 109-112)
- Remove
-
GetRaceRegistrationsUseCase.ts (lines 27-29, 31-70)
- Remove output port from constructor
- Change return type
- Remove
this.output.present()calls (lines 40-43, 66-69)
-
GetLeagueFullConfigUseCase.ts (lines 35-37, 39-92)
- Remove output port from constructor
- Change return type
- Remove
this.output.present()calls (lines 47-50, 88-91)
-
GetRaceWithSOFUseCase.ts (lines 43-45, 47-118)
- Remove output port from constructor
- Change return type
- Remove
this.output.present()calls (lines 58-61, 114-117)
-
GetRaceResultsDetailUseCase.ts (lines 41-43, 45-100)
- Remove output port from constructor
- Change return type
- Remove
this.output.present()calls (lines 56-59, 95-98)
Continue this pattern for ALL 150+ use cases listed in your original analysis.
Phase 2: Fix Controllers/Services (Add Wiring Logic)
Files to modify in apps/api/src/domain/:
-
RaceService.ts (lines 135-139)
- Update
getRaceDetail()to wire use case result to presenter - Add error handling for Result.Err cases
- Update
-
RaceProviders.ts (lines 138-144, 407-437)
- Remove adapter classes that wrap presenters
- Update provider factories to inject presenters directly to controllers
- Remove
RaceDetailOutputAdapterand similar classes
-
All other service files that use use cases
- Update method signatures to handle Results
- Add proper error mapping
- Wire results to presenters
Phase 3: Update Module Wiring
Files to modify:
-
RaceProviders.ts (lines 287-779)
- Remove all adapter classes (lines 111-285)
- Update provider definitions to not use adapters
- Simplify dependency injection
-
All other provider files in
apps/api/src/domain/*/- Remove adapter patterns
- Update DI containers
Phase 4: Fix Presenters (If Needed)
Some presenters may need updates:
-
RaceDetailPresenter.ts (lines 15-26, 28-114)
- Ensure
present()method acceptsGetRaceDetailResultdirectly - No changes needed if already correct
- Ensure
-
CommandResultPresenter.ts and similar
- Ensure they work with Results from controllers, not use cases
Implementation Checklist
For Each Use Case File:
- Remove
output: UseCaseOutputPort<T>from constructor - Change return type from
Promise<Result<void, E>>toPromise<Result<T, E>> - Remove all
this.output.present()calls - Return Result directly
- Update imports if needed
For Each Controller/Service File:
- Update methods to call use case and get Result
- Add
if (result.isErr())error handling - Call
presenter.present(result.value)after success - Return presenter or ViewModel
- Remove adapter usage
For Each Provider File:
- Remove adapter classes
- Update DI to inject presenters to controllers
- Simplify provider definitions
Files That Need Immediate Attention
High Priority (Core Racing Domain):
core/racing/application/use-cases/GetRaceDetailUseCase.ts
core/racing/application/use-cases/GetRaceRegistrationsUseCase.ts
core/racing/application/use-cases/GetLeagueFullConfigUseCase.ts
core/racing/application/use-cases/GetRaceWithSOFUseCase.ts
core/racing/application/use-cases/GetRaceResultsDetailUseCase.ts
core/racing/application/use-cases/GetLeagueAdminPermissionsUseCase.ts
core/racing/application/use-cases/CompleteRaceUseCase.ts
core/racing/application/use-cases/ApplyPenaltyUseCase.ts
core/racing/application/use-cases/JoinLeagueUseCase.ts
core/racing/application/use-cases/JoinTeamUseCase.ts
core/racing/application/use-cases/RegisterForRaceUseCase.ts
core/racing/application/use-cases/WithdrawFromRaceUseCase.ts
core/racing/application/use-cases/CancelRaceUseCase.ts
core/racing/application/use-cases/ReopenRaceUseCase.ts
core/racing/application/use-cases/CompleteRaceUseCaseWithRatings.ts
core/racing/application/use-cases/ImportRaceResultsUseCase.ts
core/racing/application/use-cases/ImportRaceResultsApiUseCase.ts
core/racing/application/use-cases/FileProtestUseCase.ts
core/racing/application/use-cases/ReviewProtestUseCase.ts
core/racing/application/use-cases/QuickPenaltyUseCase.ts
core/racing/application/use-cases/ApplyForSponsorshipUseCase.ts
core/racing/application/use-cases/AcceptSponsorshipRequestUseCase.ts
core/racing/application/use-cases/RejectSponsorshipRequestUseCase.ts
core/racing/application/use-cases/GetSponsorDashboardUseCase.ts
core/racing/application/use-cases/GetSponsorSponsorshipsUseCase.ts
core/racing/application/use-cases/GetSponsorshipPricingUseCase.ts
core/racing/application/use-cases/GetEntitySponsorshipPricingUseCase.ts
core/racing/application/use-cases/GetSeasonSponsorshipsUseCase.ts
core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase.ts
core/racing/application/use-cases/GetLeagueWalletUseCase.ts
core/racing/application/use-cases/WithdrawFromLeagueWalletUseCase.ts
core/racing/application/use-cases/GetLeagueStatsUseCase.ts
core/racing/application/use-cases/GetLeagueMembershipsUseCase.ts
core/racing/application/use-cases/GetLeagueJoinRequestsUseCase.ts
core/racing/application/use-cases/GetTeamJoinRequestsUseCase.ts
core/racing/application/use-cases/GetLeagueRosterMembersUseCase.ts
core/racing/application/use-cases/GetLeagueRosterJoinRequestsUseCase.ts
core/racing/application/use-cases/ApproveLeagueJoinRequestUseCase.ts
core/racing/application/use-cases/RejectLeagueJoinRequestUseCase.ts
core/racing/application/use-cases/UpdateLeagueMemberRoleUseCase.ts
core/racing/application/use-cases/RemoveLeagueMemberUseCase.ts
core/racing/application/use-cases/TransferLeagueOwnershipUseCase.ts
core/racing/application/use-cases/GetLeagueAdminUseCase.ts
core/racing/application/use-cases/GetLeagueOwnerSummaryUseCase.ts
core/racing/application/use-cases/GetLeagueScoringConfigUseCase.ts
core/racing/application/use-cases/ListLeagueScoringPresetsUseCase.ts
core/racing/application/use-cases/GetLeagueScheduleUseCase.ts
core/racing/application/use-cases/CreateLeagueSeasonScheduleRaceUseCase.ts
core/racing/application/use-cases/UpdateLeagueSeasonScheduleRaceUseCase.ts
core/racing/application/use-cases/DeleteLeagueSeasonScheduleRaceUseCase.ts
core/racing/application/use-cases/PublishLeagueSeasonScheduleUseCase.ts
core/racing/application/use-cases/UnpublishLeagueSeasonScheduleUseCase.ts
core/racing/application/use-cases/PreviewLeagueScheduleUseCase.ts
core/racing/application/use-cases/GetSeasonDetailsUseCase.ts
core/racing/application/use-cases/ListSeasonsForLeagueUseCase.ts
core/racing/application/use-cases/CreateSeasonForLeagueUseCase.ts
core/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase.ts
core/racing/application/use-cases/ManageSeasonLifecycleUseCase.ts
core/racing/application/use-cases/RecalculateChampionshipStandingsUseCase.ts
core/racing/application/use-cases/GetLeagueStandingsUseCase.ts
core/racing/application/use-cases/GetDriversLeaderboardUseCase.ts
core/racing/application/use-cases/GetTeamsLeaderboardUseCase.ts
core/racing/application/use-cases/GetTotalDriversUseCase.ts
core/racing/application/use-cases/GetTotalLeaguesUseCase.ts
core/racing/application/use-cases/GetTotalRacesUseCase.ts
core/racing/application/use-cases/GetAllRacesUseCase.ts
core/racing/application/use-cases/GetAllRacesPageDataUseCase.ts
core/racing/application/use-cases/GetRacesPageDataUseCase.ts
core/racing/application/use-cases/GetAllLeaguesWithCapacityAndScoringUseCase.ts
core/racing/application/use-cases/GetAllTeamsUseCase.ts
core/racing/application/use-cases/GetTeamDetailsUseCase.ts
core/racing/application/use-cases/GetTeamMembersUseCase.ts
core/racing/application/use-cases/UpdateTeamUseCase.ts
core/racing/application/use-cases/CreateTeamUseCase.ts
core/racing/application/use-cases/LeaveTeamUseCase.ts
core/racing/application/use-cases/ApproveTeamJoinRequestUseCase.ts
core/racing/application/use-cases/RejectTeamJoinRequestUseCase.ts
core/racing/application/use-cases/GetDriverTeamUseCase.ts
core/racing/application/use-cases/GetProfileOverviewUseCase.ts
core/racing/application/use-cases/CompleteDriverOnboardingUseCase.ts
core/racing/application/use-cases/UpdateDriverProfileUseCase.ts
core/racing/application/use-cases/SendFinalResultsUseCase.ts
core/racing/application/use-cases/SendPerformanceSummaryUseCase.ts
core/racing/application/use-cases/RequestProtestDefenseUseCase.ts
core/racing/application/use-cases/SubmitProtestDefenseUseCase.ts
core/racing/application/use-cases/GetRaceProtestsUseCase.ts
core/racing/application/use-cases/GetLeagueProtestsUseCase.ts
core/racing/application/use-cases/GetRacePenaltiesUseCase.ts
core/racing/application/use-cases/GetSponsorsUseCase.ts
core/racing/application/use-cases/GetSponsorUseCase.ts
core/racing/application/use-cases/CreateSponsorUseCase.ts
core/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase.ts
core/racing/application/use-cases/GetLeagueStatsUseCase.ts
Medium Priority (Media Domain):
core/media/application/use-cases/GetAvatarUseCase.ts
core/media/application/use-cases/GetMediaUseCase.ts
core/media/application/use-cases/DeleteMediaUseCase.ts
core/media/application/use-cases/UploadMediaUseCase.ts
core/media/application/use-cases/UpdateAvatarUseCase.ts
core/media/application/use-cases/RequestAvatarGenerationUseCase.ts
core/media/application/use-cases/SelectAvatarUseCase.ts
Medium Priority (Identity Domain):
core/identity/application/use-cases/SignupUseCase.ts
core/identity/application/use-cases/SignupWithEmailUseCase.ts
core/identity/application/use-cases/LoginUseCase.ts
core/identity/application/use-cases/LoginWithEmailUseCase.ts
core/identity/application/use-cases/ForgotPasswordUseCase.ts
core/identity/application/use-cases/ResetPasswordUseCase.ts
core/identity/application/use-cases/GetCurrentSessionUseCase.ts
core/identity/application/use-cases/GetCurrentUserSessionUseCase.ts
core/identity/application/use-cases/LogoutUseCase.ts
core/identity/application/use-cases/StartAuthUseCase.ts
core/identity/application/use-cases/HandleAuthCallbackUseCase.ts
core/identity/application/use-cases/SignupSponsorUseCase.ts
core/identity/application/use-cases/CreateAchievementUseCase.ts
Medium Priority (Notifications Domain):
core/notifications/application/use-cases/GetUnreadNotificationsUseCase.ts
core/notifications/application/use-cases/MarkNotificationReadUseCase.ts
core/notifications/application/use-cases/NotificationPreferencesUseCases.ts
core/notifications/application/use-cases/SendNotificationUseCase.ts
Medium Priority (Analytics Domain):
core/analytics/application/use-cases/GetAnalyticsMetricsUseCase.ts
core/analytics/application/use-cases/GetDashboardDataUseCase.ts
core/analytics/application/use-cases/RecordPageViewUseCase.ts
core/analytics/application/use-cases/RecordEngagementUseCase.ts
Medium Priority (Admin Domain):
core/admin/application/use-cases/ListUsersUseCase.ts
Medium Priority (Social Domain):
core/social/application/use-cases/GetUserFeedUseCase.ts
core/social/application/use-cases/GetCurrentUserSocialUseCase.ts
Medium Priority (Payments Domain):
core/payments/application/use-cases/GetWalletUseCase.ts
core/payments/application/use-cases/GetMembershipFeesUseCase.ts
core/payments/application/use-cases/UpdatePaymentStatusUseCase.ts
core/payments/application/use-cases/AwardPrizeUseCase.ts
core/payments/application/use-cases/DeletePrizeUseCase.ts
core/payments/application/use-cases/CreatePrizeUseCase.ts
core/payments/application/use-cases/CreatePaymentUseCase.ts
core/payments/application/use-cases/ProcessWalletTransactionUseCase.ts
core/payments/application/use-cases/UpdateMemberPaymentUseCase.ts
core/payments/application/use-cases/GetPaymentsUseCase.ts
core/payments/application/use-cases/UpsertMembershipFeeUseCase.ts
Controller/Service Files:
apps/api/src/domain/race/RaceService.ts
apps/api/src/domain/race/RaceProviders.ts
apps/api/src/domain/sponsor/SponsorService.ts
apps/api/src/domain/league/LeagueService.ts
apps/api/src/domain/driver/DriverService.ts
apps/api/src/domain/auth/AuthService.ts
apps/api/src/domain/analytics/AnalyticsService.ts
apps/api/src/domain/notifications/NotificationsService.ts
apps/api/src/domain/payments/PaymentsService.ts
apps/api/src/domain/admin/AdminService.ts
apps/api/src/domain/social/SocialService.ts
apps/api/src/domain/media/MediaService.ts
Success Criteria
✅ All use cases return Result<T, E> directly
✅ No use case calls .present()
✅ All controllers wire Results to Presenters
✅ All adapter classes removed
✅ Module wiring simplified
✅ "Presenter not presented" errors eliminated
✅ Tests updated and passing
Estimated Effort
- 150+ use cases to fix
- 20+ controller/service files to update
- 10+ provider files to simplify
- Estimated time: 2-3 days of focused work
- Risk: Medium (requires careful testing)
Next Steps
- Start with Phase 1: Fix the core racing use cases first (highest impact)
- Test each change: Run existing tests to ensure no regressions
- Update controllers: Wire Results to Presenters
- Simplify providers: Remove adapter classes
- Run full test suite: Verify everything works
This plan provides the roadmap to achieve 100% Clean Architecture compliance.