Files
gridpilot.gg/plans/CLEAN_ARCHITECTURE_FIX_PLAN.md

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/:

  1. GetRaceDetailUseCase.ts (lines 35-44, 46-115)

    • Remove output: UseCaseOutputPort<GetRaceDetailResult> from constructor
    • Change return type from Promise<Result<void, ApplicationError>> to Promise<Result<GetRaceDetailResult, ApplicationError>>
    • Remove all this.output.present() calls (lines 100, 109-112)
  2. 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)
  3. 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)
  4. 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)
  5. 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/:

  1. RaceService.ts (lines 135-139)

    • Update getRaceDetail() to wire use case result to presenter
    • Add error handling for Result.Err cases
  2. RaceProviders.ts (lines 138-144, 407-437)

    • Remove adapter classes that wrap presenters
    • Update provider factories to inject presenters directly to controllers
    • Remove RaceDetailOutputAdapter and similar classes
  3. 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:

  1. RaceProviders.ts (lines 287-779)

    • Remove all adapter classes (lines 111-285)
    • Update provider definitions to not use adapters
    • Simplify dependency injection
  2. 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:

  1. RaceDetailPresenter.ts (lines 15-26, 28-114)

    • Ensure present() method accepts GetRaceDetailResult directly
    • No changes needed if already correct
  2. 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>> to Promise<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

  1. Start with Phase 1: Fix the core racing use cases first (highest impact)
  2. Test each change: Run existing tests to ensure no regressions
  3. Update controllers: Wire Results to Presenters
  4. Simplify providers: Remove adapter classes
  5. Run full test suite: Verify everything works

This plan provides the roadmap to achieve 100% Clean Architecture compliance.