From 9bd2e630e65503db39e9977accbcb73e707fec93 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Sun, 21 Dec 2025 22:35:38 +0100 Subject: [PATCH] refactor --- .../analytics/AnalyticsController.test.ts | 26 +- .../domain/analytics/AnalyticsModule.test.ts | 6 +- .../src/domain/auth/AuthController.test.ts | 17 +- apps/api/src/domain/auth/AuthController.ts | 3 +- apps/api/src/domain/auth/AuthProviders.ts | 28 +- apps/api/src/domain/auth/AuthService.ts | 40 +-- .../presenters/AuthSessionPresenter.test.ts | 25 +- .../domain/dashboard/DashboardModule.test.ts | 8 + .../src/domain/dashboard/DashboardService.ts | 4 +- .../api/src/domain/driver/DriverController.ts | 26 +- apps/api/src/domain/driver/DriverProviders.ts | 29 ++- apps/api/src/domain/driver/DriverService.ts | 63 +++-- .../DriverRegistrationStatusPresenter.test.ts | 18 +- .../DriverRegistrationStatusPresenter.ts | 4 + .../presenters/DriversLeaderboardPresenter.ts | 37 +-- apps/api/src/domain/media/MediaProviders.ts | 93 +++++-- apps/api/src/domain/media/MediaService.ts | 93 ++++--- .../media/presenters/DeleteMediaPresenter.ts | 32 +-- .../media/presenters/GetAvatarPresenter.ts | 32 +-- .../media/presenters/GetMediaPresenter.ts | 29 +-- .../RequestAvatarGenerationPresenter.ts | 43 +--- .../media/presenters/UpdateAvatarPresenter.ts | 25 +- .../media/presenters/UploadMediaPresenter.ts | 34 +-- .../src/domain/payments/PaymentsController.ts | 9 +- .../src/domain/payments/PaymentsProviders.ts | 150 ++++++++--- .../src/domain/payments/PaymentsService.ts | 72 ++++-- .../presenters/CreatePaymentPresenter.ts | 37 ++- .../presenters/GetPaymentsPresenter.ts | 43 ++-- .../UpdatePaymentStatusPresenter.ts | 43 ++-- .../presenters/ReviewProtestPresenter.ts | 31 +-- apps/api/src/domain/race/RaceService.ts | 9 +- .../race/presenters/GetAllRacesPresenter.ts | 32 +-- .../application/use-cases/LogoutUseCase.ts | 6 +- .../use-cases/GetAllRacesUseCase.test.ts | 4 +- .../use-cases/GetAllRacesUseCase.ts | 10 +- .../use-cases/GetDriversLeaderboardUseCase.ts | 9 +- .../use-cases/UpdateDriverProfileUseCase.ts | 10 +- plans/api-usecase-presenter-migration.md | 240 +++++++++--------- 38 files changed, 736 insertions(+), 684 deletions(-) diff --git a/apps/api/src/domain/analytics/AnalyticsController.test.ts b/apps/api/src/domain/analytics/AnalyticsController.test.ts index e15a175d7..a56eeef37 100644 --- a/apps/api/src/domain/analytics/AnalyticsController.test.ts +++ b/apps/api/src/domain/analytics/AnalyticsController.test.ts @@ -12,24 +12,14 @@ describe('AnalyticsController', () => { let controller: AnalyticsController; let service: ReturnType>; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [AnalyticsController], - providers: [ - { - provide: AnalyticsService, - useValue: { - recordPageView: vi.fn(), - recordEngagement: vi.fn(), - getDashboardData: vi.fn(), - getAnalyticsMetrics: vi.fn(), - }, - }, - ], - }).compile(); - - controller = module.get(AnalyticsController); - service = vi.mocked(module.get(AnalyticsService)); + beforeEach(() => { + service = { + recordPageView: vi.fn(), + recordEngagement: vi.fn(), + getDashboardData: vi.fn(), + getAnalyticsMetrics: vi.fn(), + } as any; + controller = new AnalyticsController(service); }); describe('recordPageView', () => { diff --git a/apps/api/src/domain/analytics/AnalyticsModule.test.ts b/apps/api/src/domain/analytics/AnalyticsModule.test.ts index a0000cdc1..0cc2792fb 100644 --- a/apps/api/src/domain/analytics/AnalyticsModule.test.ts +++ b/apps/api/src/domain/analytics/AnalyticsModule.test.ts @@ -2,6 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AnalyticsModule } from './AnalyticsModule'; import { AnalyticsController } from './AnalyticsController'; import { AnalyticsService } from './AnalyticsService'; +import { ConsoleLogger } from '@adapters/logging/ConsoleLogger'; describe('AnalyticsModule', () => { let module: TestingModule; @@ -9,7 +10,10 @@ describe('AnalyticsModule', () => { beforeEach(async () => { module = await Test.createTestingModule({ imports: [AnalyticsModule], - }).compile(); + }) + .overrideProvider('Logger_TOKEN') + .useClass(ConsoleLogger) + .compile(); }); it('should compile the module', () => { diff --git a/apps/api/src/domain/auth/AuthController.test.ts b/apps/api/src/domain/auth/AuthController.test.ts index 2426c06a6..0b16d49b8 100644 --- a/apps/api/src/domain/auth/AuthController.test.ts +++ b/apps/api/src/domain/auth/AuthController.test.ts @@ -1,7 +1,8 @@ -import { vi } from 'vitest'; +import { Mock, vi } from 'vitest'; import { AuthController } from './AuthController'; import { AuthService } from './AuthService'; -import { SignupParams, LoginParams, AuthSessionDTO } from './dtos/AuthDto'; +import { AuthSessionDTO, LoginParams, SignupParams } from './dtos/AuthDto'; +import type { CommandResultDTO } from './presenters/CommandResultPresenter'; describe('AuthController', () => { let controller: AuthController; @@ -36,7 +37,7 @@ describe('AuthController', () => { displayName: 'Test User', }, }; - (service.signupWithEmail as jest.Mock).mockResolvedValue(session); + (service.signupWithEmail as Mock).mockResolvedValue(session); const result = await controller.signup(params); @@ -59,7 +60,7 @@ describe('AuthController', () => { displayName: 'Test User', }, }; - (service.loginWithEmail as jest.Mock).mockResolvedValue(session); + (service.loginWithEmail as Mock).mockResolvedValue(session); const result = await controller.login(params); @@ -78,7 +79,7 @@ describe('AuthController', () => { displayName: 'Test User', }, }; - (service.getCurrentSession as jest.Mock).mockResolvedValue(session); + (service.getCurrentSession as Mock).mockResolvedValue(session); const result = await controller.getSession(); @@ -87,7 +88,7 @@ describe('AuthController', () => { }); it('should return null if no session', async () => { - (service.getCurrentSession as jest.Mock).mockResolvedValue(null); + (service.getCurrentSession as Mock).mockResolvedValue(null); const result = await controller.getSession(); @@ -97,8 +98,8 @@ describe('AuthController', () => { describe('logout', () => { it('should call service.logout and return DTO', async () => { - const dto = { success: true }; - (service.logout as jest.Mock).mockResolvedValue(dto); + const dto: CommandResultDTO = { success: true }; + (service.logout as Mock).mockResolvedValue(dto); const result = await controller.logout(); diff --git a/apps/api/src/domain/auth/AuthController.ts b/apps/api/src/domain/auth/AuthController.ts index e16accb84..dc81f2345 100644 --- a/apps/api/src/domain/auth/AuthController.ts +++ b/apps/api/src/domain/auth/AuthController.ts @@ -1,6 +1,7 @@ import { Controller, Get, Post, Body } from '@nestjs/common'; import { AuthService } from './AuthService'; import { LoginParams, SignupParams, AuthSessionDTO } from './dtos/AuthDto'; +import type { CommandResultDTO } from './presenters/CommandResultPresenter'; @Controller('auth') export class AuthController { @@ -22,7 +23,7 @@ export class AuthController { } @Post('logout') - async logout(): Promise<{ success: boolean }> { + async logout(): Promise { return this.authService.logout(); } } diff --git a/apps/api/src/domain/auth/AuthProviders.ts b/apps/api/src/domain/auth/AuthProviders.ts index be435f779..822f827ec 100644 --- a/apps/api/src/domain/auth/AuthProviders.ts +++ b/apps/api/src/domain/auth/AuthProviders.ts @@ -31,6 +31,8 @@ export const IDENTITY_SESSION_PORT_TOKEN = 'IdentitySessionPort'; export const LOGIN_USE_CASE_TOKEN = 'LoginUseCase'; export const SIGNUP_USE_CASE_TOKEN = 'SignupUseCase'; export const LOGOUT_USE_CASE_TOKEN = 'LogoutUseCase'; +export const AUTH_SESSION_PRESENTER_TOKEN = 'AuthSessionPresenter'; +export const COMMAND_RESULT_PRESENTER_TOKEN = 'CommandResultPresenter'; export const AuthProviders: Provider[] = [ { @@ -73,20 +75,28 @@ export const AuthProviders: Provider[] = [ }, { provide: LOGIN_USE_CASE_TOKEN, - useFactory: (authRepo: IAuthRepository, passwordHashing: IPasswordHashingService, logger: Logger) => - new LoginUseCase(authRepo, passwordHashing, logger), - inject: [AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN], + useFactory: (authRepo: IAuthRepository, passwordHashing: IPasswordHashingService, logger: Logger, presenter: AuthSessionPresenter) => + new LoginUseCase(authRepo, passwordHashing, logger, presenter), + inject: [AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN, AUTH_SESSION_PRESENTER_TOKEN], }, { provide: SIGNUP_USE_CASE_TOKEN, - useFactory: (authRepo: IAuthRepository, passwordHashing: IPasswordHashingService, logger: Logger) => - new SignupUseCase(authRepo, passwordHashing, logger), - inject: [AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN], + useFactory: (authRepo: IAuthRepository, passwordHashing: IPasswordHashingService, logger: Logger, presenter: AuthSessionPresenter) => + new SignupUseCase(authRepo, passwordHashing, logger, presenter), + inject: [AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN, AUTH_SESSION_PRESENTER_TOKEN], }, { provide: LOGOUT_USE_CASE_TOKEN, - useFactory: (sessionPort: IdentitySessionPort, logger: Logger) => - new LogoutUseCase(sessionPort, logger), - inject: [IDENTITY_SESSION_PORT_TOKEN, LOGGER_TOKEN], + useFactory: (sessionPort: IdentitySessionPort, logger: Logger, presenter: CommandResultPresenter) => + new LogoutUseCase(sessionPort, logger, presenter), + inject: [IDENTITY_SESSION_PORT_TOKEN, LOGGER_TOKEN, COMMAND_RESULT_PRESENTER_TOKEN], + }, + { + provide: AUTH_SESSION_PRESENTER_TOKEN, + useClass: AuthSessionPresenter, + }, + { + provide: COMMAND_RESULT_PRESENTER_TOKEN, + useClass: CommandResultPresenter, }, ]; diff --git a/apps/api/src/domain/auth/AuthService.ts b/apps/api/src/domain/auth/AuthService.ts index 0c9450929..58b282726 100644 --- a/apps/api/src/domain/auth/AuthService.ts +++ b/apps/api/src/domain/auth/AuthService.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Inject } from '@nestjs/common'; // Core Use Cases import { LoginUseCase, type LoginInput } from '@core/identity/application/use-cases/LoginUseCase'; @@ -11,12 +11,10 @@ import { User } from '@core/identity/domain/entities/User'; import type { IUserRepository } from '@core/identity/domain/repositories/IUserRepository'; import type { Logger } from '@core/shared/application'; import { IDENTITY_SESSION_PORT_TOKEN, LOGGER_TOKEN, LOGIN_USE_CASE_TOKEN, LOGOUT_USE_CASE_TOKEN, SIGNUP_USE_CASE_TOKEN, USER_REPOSITORY_TOKEN } from './AuthProviders'; -import { AuthenticatedUserDTO, AuthSessionDTO, LoginParams, SignupParams } from './dtos/AuthDto'; +import { AuthSessionDTO, LoginParams, SignupParams } from './dtos/AuthDto'; import { AuthSessionPresenter } from './presenters/AuthSessionPresenter'; -import type { CommandResultDTO } from './presenters/CommandResultPresenter'; -import { CommandResultPresenter } from './presenters/CommandResultPresenter'; +import { CommandResultPresenter, type CommandResultDTO } from './presenters/CommandResultPresenter'; -@Injectable() export class AuthService { constructor( @Inject(LOGGER_TOKEN) private logger: Logger, @@ -25,31 +23,10 @@ export class AuthService { @Inject(LOGIN_USE_CASE_TOKEN) private readonly loginUseCase: LoginUseCase, @Inject(SIGNUP_USE_CASE_TOKEN) private readonly signupUseCase: SignupUseCase, @Inject(LOGOUT_USE_CASE_TOKEN) private readonly logoutUseCase: LogoutUseCase, - private readonly authSessionPresenter: AuthSessionPresenter, - private readonly commandResultPresenter: CommandResultPresenter, ) {} - private mapUserToAuthenticatedUserDTO(user: User): AuthenticatedUserDTO { - return { - userId: user.getId().value, - email: user.getEmail() ?? '', - displayName: user.getDisplayName() ?? '', - }; - } - - - private buildAuthSessionDTO(token: string, user: AuthenticatedUserDTO): AuthSessionDTO { - return { - token, - user: { - userId: user.userId, - email: user.email, - displayName: user.displayName, - }, - }; - } - async getCurrentSession(): Promise { + // TODO this must call a use case this.logger.debug('[AuthService] Attempting to get current session.'); const coreSession = await this.identitySessionPort.getCurrentSession(); if (!coreSession) { @@ -87,7 +64,8 @@ export class AuthService { throw new Error(error.details?.message ?? 'Signup failed'); } - const userDTO = this.authSessionPresenter.getResponseModel(); + const authSessionPresenter = new AuthSessionPresenter(); + const userDTO = authSessionPresenter.getResponseModel(); const coreUserDTO = { id: userDTO.userId, displayName: userDTO.displayName, @@ -116,7 +94,8 @@ export class AuthService { throw new Error(error.details?.message ?? 'Login failed'); } - const userDTO = this.authSessionPresenter.getResponseModel(); + const authSessionPresenter = new AuthSessionPresenter(); + const userDTO = authSessionPresenter.getResponseModel(); const coreUserDTO = { id: userDTO.userId, displayName: userDTO.displayName, @@ -133,6 +112,7 @@ export class AuthService { async logout(): Promise { this.logger.debug('[AuthService] Attempting logout.'); + const commandResultPresenter = new CommandResultPresenter(); const result = await this.logoutUseCase.execute(); if (result.isErr()) { @@ -140,6 +120,6 @@ export class AuthService { throw new Error(error.details?.message ?? 'Logout failed'); } - return this.commandResultPresenter.getResponseModel(); + return commandResultPresenter.getResponseModel(); } } diff --git a/apps/api/src/domain/auth/presenters/AuthSessionPresenter.test.ts b/apps/api/src/domain/auth/presenters/AuthSessionPresenter.test.ts index 9f7e9eb8d..10172c3c3 100644 --- a/apps/api/src/domain/auth/presenters/AuthSessionPresenter.test.ts +++ b/apps/api/src/domain/auth/presenters/AuthSessionPresenter.test.ts @@ -5,16 +5,12 @@ import { UserId } from '@core/identity/domain/value-objects/UserId'; describe('AuthSessionPresenter', () => { let presenter: AuthSessionPresenter; - let mockIdentitySessionPort: any; beforeEach(() => { - mockIdentitySessionPort = { - createSession: vi.fn(), - }; - presenter = new AuthSessionPresenter(mockIdentitySessionPort); + presenter = new AuthSessionPresenter(); }); - it('maps successful result into response model', async () => { + it('maps successful result into response model', () => { const user = User.create({ id: UserId.fromString('user-1'), displayName: 'Test User', @@ -22,20 +18,15 @@ describe('AuthSessionPresenter', () => { passwordHash: { value: 'hash' } as any, }); - const expectedSession = { - token: 'token-123', - user: { - userId: 'user-1', - email: 'user@example.com', - displayName: 'Test User', - }, + const expectedUser = { + userId: 'user-1', + email: 'user@example.com', + displayName: 'Test User', }; - mockIdentitySessionPort.createSession.mockResolvedValue(expectedSession); + presenter.present({ user }); - await presenter.present({ user }); - - expect(presenter.getResponseModel()).toEqual(expectedSession); + expect(presenter.getResponseModel()).toEqual(expectedUser); }); it('getResponseModel throws when not presented', () => { diff --git a/apps/api/src/domain/dashboard/DashboardModule.test.ts b/apps/api/src/domain/dashboard/DashboardModule.test.ts index c01ec893a..cf5b4a21c 100644 --- a/apps/api/src/domain/dashboard/DashboardModule.test.ts +++ b/apps/api/src/domain/dashboard/DashboardModule.test.ts @@ -2,6 +2,8 @@ import { Test, TestingModule } from '@nestjs/testing'; import { DashboardModule } from './DashboardModule'; import { DashboardController } from './DashboardController'; import { DashboardService } from './DashboardService'; +import { DashboardOverviewPresenter } from './presenters/DashboardOverviewPresenter'; +import { DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN } from './DashboardProviders'; describe('DashboardModule', () => { let module: TestingModule; @@ -27,4 +29,10 @@ describe('DashboardModule', () => { expect(service).toBeDefined(); expect(service).toBeInstanceOf(DashboardService); }); + + it('should bind DashboardOverviewPresenter as the output port for the use case', () => { + const presenter = module.get(DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN); + expect(presenter).toBeDefined(); + expect(presenter).toBeInstanceOf(DashboardOverviewPresenter); + }); }); \ No newline at end of file diff --git a/apps/api/src/domain/dashboard/DashboardService.ts b/apps/api/src/domain/dashboard/DashboardService.ts index 9fe8f07b7..198a6ac57 100644 --- a/apps/api/src/domain/dashboard/DashboardService.ts +++ b/apps/api/src/domain/dashboard/DashboardService.ts @@ -7,14 +7,14 @@ import { DashboardOverviewPresenter } from './presenters/DashboardOverviewPresen import type { Logger } from '@core/shared/application/Logger'; // Tokens -import { LOGGER_TOKEN, DASHBOARD_OVERVIEW_USE_CASE_TOKEN } from './DashboardProviders'; +import { LOGGER_TOKEN, DASHBOARD_OVERVIEW_USE_CASE_TOKEN, DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN } from './DashboardProviders'; @Injectable() export class DashboardService { constructor( @Inject(LOGGER_TOKEN) private readonly logger: Logger, @Inject(DASHBOARD_OVERVIEW_USE_CASE_TOKEN) private readonly dashboardOverviewUseCase: DashboardOverviewUseCase, - private readonly dashboardOverviewPresenter: DashboardOverviewPresenter, + @Inject(DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN) private readonly dashboardOverviewPresenter: DashboardOverviewPresenter, ) {} async getDashboardOverview(driverId: string): Promise { diff --git a/apps/api/src/domain/driver/DriverController.ts b/apps/api/src/domain/driver/DriverController.ts index 68448af30..dd3848e48 100644 --- a/apps/api/src/domain/driver/DriverController.ts +++ b/apps/api/src/domain/driver/DriverController.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Post, Body, Req, Param } from '@nestjs/common'; +import { Controller, Get, Post, Put, Body, Req, Param } from '@nestjs/common'; import { Request } from 'express'; import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger'; @@ -25,16 +25,14 @@ export class DriverController { @ApiOperation({ summary: 'Get drivers leaderboard' }) @ApiResponse({ status: 200, description: 'List of drivers for the leaderboard', type: DriversLeaderboardDTO }) async getDriversLeaderboard(): Promise { - const presenter = await this.driverService.getDriversLeaderboard(); - return presenter.viewModel; + return await this.driverService.getDriversLeaderboard(); } @Get('total-drivers') @ApiOperation({ summary: 'Get the total number of drivers' }) @ApiResponse({ status: 200, description: 'Total number of drivers', type: DriverStatsDTO }) async getTotalDrivers(): Promise { - const presenter = await this.driverService.getTotalDrivers(); - return presenter.viewModel; + return await this.driverService.getTotalDrivers(); } @Get('current') @@ -47,8 +45,7 @@ export class DriverController { return null; } - const presenter = await this.driverService.getCurrentDriver(userId); - return presenter.viewModel; + return await this.driverService.getCurrentDriver(userId); } @Post('complete-onboarding') @@ -59,8 +56,7 @@ export class DriverController { @Req() req: AuthenticatedRequest, ): Promise { const userId = req.user!.userId; - const presenter = await this.driverService.completeOnboarding(userId, input); - return presenter.viewModel; + return await this.driverService.completeOnboarding(userId, input); } @Get(':driverId/races/:raceId/registration-status') @@ -70,8 +66,7 @@ export class DriverController { @Param('driverId') driverId: string, @Param('raceId') raceId: string, ): Promise { - const presenter = await this.driverService.getDriverRegistrationStatus({ driverId, raceId }); - return presenter.viewModel; + return await this.driverService.getDriverRegistrationStatus({ driverId, raceId }); } @Get(':driverId') @@ -79,8 +74,7 @@ export class DriverController { @ApiResponse({ status: 200, description: 'Driver data', type: GetDriverOutputDTO }) @ApiResponse({ status: 404, description: 'Driver not found' }) async getDriver(@Param('driverId') driverId: string): Promise { - const presenter = await this.driverService.getDriver(driverId); - return presenter.viewModel; + return await this.driverService.getDriver(driverId); } @Get(':driverId/profile') @@ -88,8 +82,7 @@ export class DriverController { @ApiResponse({ status: 200, description: 'Driver profile data', type: GetDriverProfileOutputDTO }) @ApiResponse({ status: 404, description: 'Driver not found' }) async getDriverProfile(@Param('driverId') driverId: string): Promise { - const presenter = await this.driverService.getDriverProfile(driverId); - return presenter.viewModel; + return await this.driverService.getDriverProfile(driverId); } @Put(':driverId/profile') @@ -99,8 +92,7 @@ export class DriverController { @Param('driverId') driverId: string, @Body() body: { bio?: string; country?: string }, ): Promise { - const presenter = await this.driverService.updateDriverProfile(driverId, body.bio, body.country); - return presenter.viewModel; + return await this.driverService.updateDriverProfile(driverId, body.bio, body.country); } // Add other Driver endpoints here based on other presenters diff --git a/apps/api/src/domain/driver/DriverProviders.ts b/apps/api/src/domain/driver/DriverProviders.ts index a3a3d2e12..5c978df3a 100644 --- a/apps/api/src/domain/driver/DriverProviders.ts +++ b/apps/api/src/domain/driver/DriverProviders.ts @@ -69,6 +69,13 @@ export const GET_PROFILE_OVERVIEW_USE_CASE_TOKEN = 'GetProfileOverviewUseCase'; export const DriverProviders: Provider[] = [ DriverService, // Provide the service itself + // Presenters + DriversLeaderboardPresenter, + DriverStatsPresenter, + CompleteOnboardingPresenter, + DriverRegistrationStatusPresenter, + DriverPresenter, + DriverProfilePresenter, { provide: DRIVER_REPOSITORY_TOKEN, useFactory: (logger: Logger) => new InMemoryDriverRepository(logger), // Factory for InMemoryDriverRepository @@ -138,8 +145,9 @@ export const DriverProviders: Provider[] = [ driverStatsService: IDriverStatsService, imageService: IImageServicePort, logger: Logger, - ) => new GetDriversLeaderboardUseCase(driverRepo, rankingService, driverStatsService, imageService, logger), - inject: [DRIVER_REPOSITORY_TOKEN, RANKING_SERVICE_TOKEN, DRIVER_STATS_SERVICE_TOKEN, IMAGE_SERVICE_PORT_TOKEN, LOGGER_TOKEN], + presenter: DriversLeaderboardPresenter, + ) => new GetDriversLeaderboardUseCase(driverRepo, rankingService, driverStatsService, imageService, logger, presenter), + inject: [DRIVER_REPOSITORY_TOKEN, RANKING_SERVICE_TOKEN, DRIVER_STATS_SERVICE_TOKEN, IMAGE_SERVICE_PORT_TOKEN, LOGGER_TOKEN, DriversLeaderboardPresenter.name], }, { provide: GET_TOTAL_DRIVERS_USE_CASE_TOKEN, @@ -148,19 +156,19 @@ export const DriverProviders: Provider[] = [ }, { provide: COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN, - useFactory: (driverRepo: IDriverRepository) => new CompleteDriverOnboardingUseCase(driverRepo), - inject: [DRIVER_REPOSITORY_TOKEN], + useFactory: (driverRepo: IDriverRepository, logger: Logger, presenter: CompleteOnboardingPresenter) => new CompleteDriverOnboardingUseCase(driverRepo, logger, presenter), + inject: [DRIVER_REPOSITORY_TOKEN, LOGGER_TOKEN, CompleteOnboardingPresenter.name], }, { provide: IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN, - useFactory: (registrationRepo: IRaceRegistrationRepository, logger: Logger) => - new IsDriverRegisteredForRaceUseCase(registrationRepo, logger), - inject: [RACE_REGISTRATION_REPOSITORY_TOKEN, LOGGER_TOKEN], + useFactory: (registrationRepo: IRaceRegistrationRepository, logger: Logger, presenter: DriverRegistrationStatusPresenter) => + new IsDriverRegisteredForRaceUseCase(registrationRepo, logger, presenter), + inject: [RACE_REGISTRATION_REPOSITORY_TOKEN, LOGGER_TOKEN, DriverRegistrationStatusPresenter.name], }, { provide: UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN, - useFactory: (driverRepo: IDriverRepository) => new UpdateDriverProfileUseCase(driverRepo), - inject: [DRIVER_REPOSITORY_TOKEN], + useFactory: (driverRepo: IDriverRepository, presenter: DriverPresenter, logger: Logger) => new UpdateDriverProfileUseCase(driverRepo, presenter, logger), + inject: [DRIVER_REPOSITORY_TOKEN, DriverPresenter.name, LOGGER_TOKEN], }, { provide: GET_PROFILE_OVERVIEW_USE_CASE_TOKEN, @@ -173,6 +181,7 @@ export const DriverProviders: Provider[] = [ driverExtendedProfileProvider: DriverExtendedProfileProvider, driverStatsService: IDriverStatsService, rankingService: IRankingService, + presenter: DriverProfilePresenter, ) => new GetProfileOverviewUseCase( driverRepo, @@ -207,6 +216,7 @@ export const DriverProviders: Provider[] = [ rating: ranking.rating, overallRank: ranking.overallRank, })), + presenter, ), inject: [ DRIVER_REPOSITORY_TOKEN, @@ -217,6 +227,7 @@ export const DriverProviders: Provider[] = [ DRIVER_EXTENDED_PROFILE_PROVIDER_TOKEN, DRIVER_STATS_SERVICE_TOKEN, RANKING_SERVICE_TOKEN, + DriverProfilePresenter.name, ], }, ]; diff --git a/apps/api/src/domain/driver/DriverService.ts b/apps/api/src/domain/driver/DriverService.ts index 50f3a0dd1..fda401fb2 100644 --- a/apps/api/src/domain/driver/DriverService.ts +++ b/apps/api/src/domain/driver/DriverService.ts @@ -14,7 +14,7 @@ import { GetTotalDriversUseCase } from '@core/racing/application/use-cases/GetTo 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'; -import { UpdateDriverProfileUseCase } from '@core/racing/application/use-cases/UpdateDriverProfileUseCase'; +import { UpdateDriverProfileUseCase, type UpdateDriverProfileInput } from '@core/racing/application/use-cases/UpdateDriverProfileUseCase'; // Presenters import { DriverStatsPresenter } from './presenters/DriverStatsPresenter'; @@ -70,10 +70,12 @@ export class DriverService { const result = await this.getDriversLeaderboardUseCase.execute({}); - const presenter = new DriversLeaderboardPresenter(); - presenter.present(result); + if (result.isErr()) { + const error = result.unwrapErr(); + throw new Error(error.details?.message ?? 'Failed to load drivers leaderboard'); + } - return presenter.getResponseModel(); + return this.driversLeaderboardPresenter.getResponseModel(); } async getTotalDrivers(): Promise { @@ -101,14 +103,15 @@ export class DriverService { lastName: input.lastName, displayName: input.displayName, country: input.country, - timezone: input.timezone, - bio: input.bio, + ...(input.bio !== undefined ? { bio: input.bio } : {}), }); - const presenter = new CompleteOnboardingPresenter(); - presenter.present(result); + if (result.isErr()) { + const error = result.unwrapErr(); + throw new Error(error.details?.message ?? 'Failed to complete onboarding'); + } - return presenter.responseModel; + return this.completeOnboardingPresenter.getResponseModel(); } async getDriverRegistrationStatus( @@ -121,10 +124,12 @@ export class DriverService { driverId: query.driverId, }); - const presenter = new DriverRegistrationStatusPresenter(); - presenter.present(result); + if (result.isErr()) { + const error = result.unwrapErr(); + throw new Error(error.details?.message ?? 'Failed to check registration status'); + } - return presenter.responseModel; + return this.driverRegistrationStatusPresenter.getResponseModel(); } async getCurrentDriver(userId: string): Promise { @@ -132,10 +137,9 @@ export class DriverService { const driver = await this.driverRepository.findById(userId); - const presenter = new DriverPresenter(); - presenter.present(driver ?? null); + this.driverPresenter.present(driver ?? null); - return presenter.responseModel; + return this.driverPresenter.getResponseModel(); } async updateDriverProfile( @@ -145,19 +149,21 @@ export class DriverService { ): Promise { this.logger.debug(`[DriverService] Updating driver profile for driverId: ${driverId}`); - const result = await this.updateDriverProfileUseCase.execute({ driverId, bio, country }); + const input: UpdateDriverProfileInput = { driverId }; + if (bio !== undefined) input.bio = bio; + if (country !== undefined) input.country = country; - const presenter = new DriverPresenter(); + const result = await this.updateDriverProfileUseCase.execute(input); if (result.isErr()) { - this.logger.error(`Failed to update driver profile: ${result.error.code}`); - presenter.present(null); - return presenter.responseModel; + this.logger.error(`Failed to update driver profile: ${result.unwrapErr().code}`); + this.driverPresenter.present(null); + return this.driverPresenter.getResponseModel(); } const updatedDriver = await this.driverRepository.findById(driverId); - presenter.present(updatedDriver ?? null); - return presenter.responseModel; + this.driverPresenter.present(updatedDriver ?? null); + return this.driverPresenter.getResponseModel(); } async getDriver(driverId: string): Promise { @@ -165,10 +171,9 @@ export class DriverService { const driver = await this.driverRepository.findById(driverId); - const presenter = new DriverPresenter(); - presenter.present(driver ?? null); + this.driverPresenter.present(driver ?? null); - return presenter.responseModel; + return this.driverPresenter.getResponseModel(); } async getDriverProfile(driverId: string): Promise { @@ -176,9 +181,11 @@ export class DriverService { const result = await this.getProfileOverviewUseCase.execute({ driverId }); - const presenter = new DriverProfilePresenter(); - presenter.present(result); + if (result.isErr()) { + const error = result.unwrapErr(); + throw new Error(error.details?.message ?? 'Failed to load driver profile'); + } - return presenter.responseModel; + return this.driverProfilePresenter.getResponseModel(); } } diff --git a/apps/api/src/domain/driver/presenters/DriverRegistrationStatusPresenter.test.ts b/apps/api/src/domain/driver/presenters/DriverRegistrationStatusPresenter.test.ts index bd7f81608..2b6a94725 100644 --- a/apps/api/src/domain/driver/presenters/DriverRegistrationStatusPresenter.test.ts +++ b/apps/api/src/domain/driver/presenters/DriverRegistrationStatusPresenter.test.ts @@ -9,10 +9,10 @@ describe('DriverRegistrationStatusPresenter', () => { }); describe('present', () => { - it('should map parameters to view model for registered driver', () => { - presenter.present(true, 'race-123', 'driver-456'); + it('should map parameters to response model for registered driver', () => { + presenter.present({ isRegistered: true, raceId: 'race-123', driverId: 'driver-456' }); - const result = presenter.viewModel; + const result = presenter.getResponseModel(); expect(result).toEqual({ isRegistered: true, @@ -21,10 +21,10 @@ describe('DriverRegistrationStatusPresenter', () => { }); }); - it('should map parameters to view model for unregistered driver', () => { - presenter.present(false, 'race-789', 'driver-101'); + it('should map parameters to response model for unregistered driver', () => { + presenter.present({ isRegistered: false, raceId: 'race-789', driverId: 'driver-101' }); - const result = presenter.viewModel; + const result = presenter.getResponseModel(); expect(result).toEqual({ isRegistered: false, @@ -36,11 +36,11 @@ describe('DriverRegistrationStatusPresenter', () => { describe('reset', () => { it('should reset the result', () => { - presenter.present(true, 'race-123', 'driver-456'); - expect(presenter.viewModel).toBeDefined(); + presenter.present({ isRegistered: true, raceId: 'race-123', driverId: 'driver-456' }); + expect(presenter.getResponseModel()).toBeDefined(); presenter.reset(); - expect(() => presenter.viewModel).toThrow('Presenter not presented'); + expect(() => presenter.getResponseModel()).toThrow('Presenter not presented'); }); }); }); \ 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 index fc64ef644..26069fa3e 100644 --- a/apps/api/src/domain/driver/presenters/DriverRegistrationStatusPresenter.ts +++ b/apps/api/src/domain/driver/presenters/DriverRegistrationStatusPresenter.ts @@ -9,6 +9,10 @@ export class DriverRegistrationStatusPresenter { private responseModel: DriverRegistrationStatusDTO | null = null; + reset(): void { + this.responseModel = null; + } + present(result: IsDriverRegisteredForRaceResult): void { this.responseModel = { isRegistered: result.isRegistered, diff --git a/apps/api/src/domain/driver/presenters/DriversLeaderboardPresenter.ts b/apps/api/src/domain/driver/presenters/DriversLeaderboardPresenter.ts index db4f426b7..91aea5b7f 100644 --- a/apps/api/src/domain/driver/presenters/DriversLeaderboardPresenter.ts +++ b/apps/api/src/domain/driver/presenters/DriversLeaderboardPresenter.ts @@ -1,29 +1,15 @@ -import type { Result } from '@core/shared/application/Result'; -import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import { DriversLeaderboardDTO } from '../dtos/DriversLeaderboardDTO'; import type { GetDriversLeaderboardResult, - GetDriversLeaderboardErrorCode, } from '@core/racing/application/use-cases/GetDriversLeaderboardUseCase'; +import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort'; -export type DriversLeaderboardApplicationError = ApplicationErrorCode< - GetDriversLeaderboardErrorCode, - { message: string } ->; +export class DriversLeaderboardPresenter implements UseCaseOutputPort { + private responseModel: DriversLeaderboardDTO | null = null; -export class DriversLeaderboardPresenter { - present( - result: Result, - ): DriversLeaderboardDTO { - if (result.isErr()) { - const error = result.unwrapErr(); - throw new Error(error.details?.message ?? 'Failed to load drivers leaderboard'); - } - - const output = result.unwrap(); - - return { - drivers: output.items.map(item => ({ + present(result: GetDriversLeaderboardResult): void { + this.responseModel = { + drivers: result.items.map(item => ({ id: item.driver.id, name: item.driver.name.toString(), rating: item.rating, @@ -36,9 +22,14 @@ export class DriversLeaderboardPresenter { rank: item.rank, avatarUrl: item.avatarUrl, })), - totalRaces: output.items.reduce((sum, d) => sum + d.racesCompleted, 0), - totalWins: output.items.reduce((sum, d) => sum + d.wins, 0), - activeCount: output.items.filter(d => d.isActive).length, + totalRaces: result.totalRaces, + totalWins: result.totalWins, + activeCount: result.activeCount, }; } + + getResponseModel(): DriversLeaderboardDTO { + if (!this.responseModel) throw new Error('Presenter not presented'); + return this.responseModel; + } } \ No newline at end of file diff --git a/apps/api/src/domain/media/MediaProviders.ts b/apps/api/src/domain/media/MediaProviders.ts index b2bcca5f6..af254c24f 100644 --- a/apps/api/src/domain/media/MediaProviders.ts +++ b/apps/api/src/domain/media/MediaProviders.ts @@ -8,7 +8,7 @@ import { IAvatarRepository } from '@core/media/domain/repositories/IAvatarReposi import { FaceValidationPort } from '@core/media/application/ports/FaceValidationPort'; import { AvatarGenerationPort } from '@core/media/application/ports/AvatarGenerationPort'; import { MediaStoragePort } from '@core/media/application/ports/MediaStoragePort'; -import type { Logger } from '@core/shared/application'; +import type { Logger, UseCaseOutputPort } from '@core/shared/application'; // Import use cases import { RequestAvatarGenerationUseCase } from '@core/media/application/use-cases/RequestAvatarGenerationUseCase'; @@ -18,6 +18,22 @@ import { DeleteMediaUseCase } from '@core/media/application/use-cases/DeleteMedi import { GetAvatarUseCase } from '@core/media/application/use-cases/GetAvatarUseCase'; import { UpdateAvatarUseCase } from '@core/media/application/use-cases/UpdateAvatarUseCase'; +// Import result types +import type { RequestAvatarGenerationResult } from '@core/media/application/use-cases/RequestAvatarGenerationUseCase'; +import type { UploadMediaResult } from '@core/media/application/use-cases/UploadMediaUseCase'; +import type { GetMediaResult } from '@core/media/application/use-cases/GetMediaUseCase'; +import type { DeleteMediaResult } from '@core/media/application/use-cases/DeleteMediaUseCase'; +import type { GetAvatarResult } from '@core/media/application/use-cases/GetAvatarUseCase'; +import type { UpdateAvatarResult } from '@core/media/application/use-cases/UpdateAvatarUseCase'; + +// Import presenters +import { RequestAvatarGenerationPresenter } from './presenters/RequestAvatarGenerationPresenter'; +import { UploadMediaPresenter } from './presenters/UploadMediaPresenter'; +import { GetMediaPresenter } from './presenters/GetMediaPresenter'; +import { DeleteMediaPresenter } from './presenters/DeleteMediaPresenter'; +import { GetAvatarPresenter } from './presenters/GetAvatarPresenter'; +import { UpdateAvatarPresenter } from './presenters/UpdateAvatarPresenter'; + // Define injection tokens export const AVATAR_GENERATION_REPOSITORY_TOKEN = 'IAvatarGenerationRepository'; export const MEDIA_REPOSITORY_TOKEN = 'IMediaRepository'; @@ -35,6 +51,14 @@ export const DELETE_MEDIA_USE_CASE_TOKEN = 'DeleteMediaUseCase'; export const GET_AVATAR_USE_CASE_TOKEN = 'GetAvatarUseCase'; export const UPDATE_AVATAR_USE_CASE_TOKEN = 'UpdateAvatarUseCase'; +// Output port tokens +export const REQUEST_AVATAR_GENERATION_OUTPUT_PORT_TOKEN = 'RequestAvatarGenerationOutputPort'; +export const UPLOAD_MEDIA_OUTPUT_PORT_TOKEN = 'UploadMediaOutputPort'; +export const GET_MEDIA_OUTPUT_PORT_TOKEN = 'GetMediaOutputPort'; +export const DELETE_MEDIA_OUTPUT_PORT_TOKEN = 'DeleteMediaOutputPort'; +export const GET_AVATAR_OUTPUT_PORT_TOKEN = 'GetAvatarOutputPort'; +export const UPDATE_AVATAR_OUTPUT_PORT_TOKEN = 'UpdateAvatarOutputPort'; + import type { AvatarGenerationRequest } from '@core/media/domain/entities/AvatarGenerationRequest'; import type { Media } from '@core/media/domain/entities/Media'; import type { Avatar } from '@core/media/domain/entities/Avatar'; @@ -110,6 +134,12 @@ class MockLogger implements Logger { export const MediaProviders: Provider[] = [ MediaService, // Provide the service itself + RequestAvatarGenerationPresenter, + UploadMediaPresenter, + GetMediaPresenter, + DeleteMediaPresenter, + GetAvatarPresenter, + UpdateAvatarPresenter, { provide: AVATAR_GENERATION_REPOSITORY_TOKEN, useClass: MockAvatarGenerationRepository, @@ -138,41 +168,66 @@ export const MediaProviders: Provider[] = [ provide: LOGGER_TOKEN, useClass: MockLogger, }, + // Output ports + { + provide: REQUEST_AVATAR_GENERATION_OUTPUT_PORT_TOKEN, + useExisting: RequestAvatarGenerationPresenter, + }, + { + provide: UPLOAD_MEDIA_OUTPUT_PORT_TOKEN, + useExisting: UploadMediaPresenter, + }, + { + provide: GET_MEDIA_OUTPUT_PORT_TOKEN, + useExisting: GetMediaPresenter, + }, + { + provide: DELETE_MEDIA_OUTPUT_PORT_TOKEN, + useExisting: DeleteMediaPresenter, + }, + { + provide: GET_AVATAR_OUTPUT_PORT_TOKEN, + useExisting: GetAvatarPresenter, + }, + { + provide: UPDATE_AVATAR_OUTPUT_PORT_TOKEN, + useExisting: UpdateAvatarPresenter, + }, // Use cases { provide: REQUEST_AVATAR_GENERATION_USE_CASE_TOKEN, - useFactory: (avatarRepo: IAvatarGenerationRepository, faceValidation: FaceValidationPort, avatarGeneration: AvatarGenerationPort, logger: Logger) => - new RequestAvatarGenerationUseCase(avatarRepo, faceValidation, avatarGeneration, logger), - inject: [AVATAR_GENERATION_REPOSITORY_TOKEN, FACE_VALIDATION_PORT_TOKEN, AVATAR_GENERATION_PORT_TOKEN, LOGGER_TOKEN], + useFactory: (avatarRepo: IAvatarGenerationRepository, faceValidation: FaceValidationPort, avatarGeneration: AvatarGenerationPort, output: UseCaseOutputPort, logger: Logger) => + new RequestAvatarGenerationUseCase(avatarRepo, faceValidation, avatarGeneration, output, logger), + inject: [AVATAR_GENERATION_REPOSITORY_TOKEN, FACE_VALIDATION_PORT_TOKEN, AVATAR_GENERATION_PORT_TOKEN, REQUEST_AVATAR_GENERATION_OUTPUT_PORT_TOKEN, LOGGER_TOKEN], }, { provide: UPLOAD_MEDIA_USE_CASE_TOKEN, - useFactory: (mediaRepo: IMediaRepository, mediaStorage: MediaStoragePort, logger: Logger) => - new UploadMediaUseCase(mediaRepo, mediaStorage, logger), - inject: [MEDIA_REPOSITORY_TOKEN, MEDIA_STORAGE_PORT_TOKEN, LOGGER_TOKEN], + useFactory: (mediaRepo: IMediaRepository, mediaStorage: MediaStoragePort, output: UseCaseOutputPort, logger: Logger) => + new UploadMediaUseCase(mediaRepo, mediaStorage, output, logger), + inject: [MEDIA_REPOSITORY_TOKEN, MEDIA_STORAGE_PORT_TOKEN, UPLOAD_MEDIA_OUTPUT_PORT_TOKEN, LOGGER_TOKEN], }, { provide: GET_MEDIA_USE_CASE_TOKEN, - useFactory: (mediaRepo: IMediaRepository, logger: Logger) => - new GetMediaUseCase(mediaRepo, logger), - inject: [MEDIA_REPOSITORY_TOKEN, LOGGER_TOKEN], + useFactory: (mediaRepo: IMediaRepository, output: UseCaseOutputPort, logger: Logger) => + new GetMediaUseCase(mediaRepo, output, logger), + inject: [MEDIA_REPOSITORY_TOKEN, GET_MEDIA_OUTPUT_PORT_TOKEN, LOGGER_TOKEN], }, { provide: DELETE_MEDIA_USE_CASE_TOKEN, - useFactory: (mediaRepo: IMediaRepository, mediaStorage: MediaStoragePort, logger: Logger) => - new DeleteMediaUseCase(mediaRepo, mediaStorage, logger), - inject: [MEDIA_REPOSITORY_TOKEN, MEDIA_STORAGE_PORT_TOKEN, LOGGER_TOKEN], + useFactory: (mediaRepo: IMediaRepository, mediaStorage: MediaStoragePort, output: UseCaseOutputPort, logger: Logger) => + new DeleteMediaUseCase(mediaRepo, mediaStorage, output, logger), + inject: [MEDIA_REPOSITORY_TOKEN, MEDIA_STORAGE_PORT_TOKEN, DELETE_MEDIA_OUTPUT_PORT_TOKEN, LOGGER_TOKEN], }, { provide: GET_AVATAR_USE_CASE_TOKEN, - useFactory: (avatarRepo: IAvatarRepository, logger: Logger) => - new GetAvatarUseCase(avatarRepo, logger), - inject: [AVATAR_REPOSITORY_TOKEN, LOGGER_TOKEN], + useFactory: (avatarRepo: IAvatarRepository, output: UseCaseOutputPort, logger: Logger) => + new GetAvatarUseCase(avatarRepo, output, logger), + inject: [AVATAR_REPOSITORY_TOKEN, GET_AVATAR_OUTPUT_PORT_TOKEN, LOGGER_TOKEN], }, { provide: UPDATE_AVATAR_USE_CASE_TOKEN, - useFactory: (avatarRepo: IAvatarRepository, logger: Logger) => - new UpdateAvatarUseCase(avatarRepo, logger), - inject: [AVATAR_REPOSITORY_TOKEN, LOGGER_TOKEN], + useFactory: (avatarRepo: IAvatarRepository, output: UseCaseOutputPort, logger: Logger) => + new UpdateAvatarUseCase(avatarRepo, output, logger), + inject: [AVATAR_REPOSITORY_TOKEN, UPDATE_AVATAR_OUTPUT_PORT_TOKEN, LOGGER_TOKEN], }, ]; diff --git a/apps/api/src/domain/media/MediaService.ts b/apps/api/src/domain/media/MediaService.ts index 131391029..c539f4ec8 100644 --- a/apps/api/src/domain/media/MediaService.ts +++ b/apps/api/src/domain/media/MediaService.ts @@ -59,6 +59,12 @@ export class MediaService { private readonly updateAvatarUseCase: UpdateAvatarUseCase, @Inject(LOGGER_TOKEN) private readonly logger: Logger, + private readonly requestAvatarGenerationPresenter: RequestAvatarGenerationPresenter, + private readonly uploadMediaPresenter: UploadMediaPresenter, + private readonly getMediaPresenter: GetMediaPresenter, + private readonly deleteMediaPresenter: DeleteMediaPresenter, + private readonly getAvatarPresenter: GetAvatarPresenter, + private readonly updateAvatarPresenter: UpdateAvatarPresenter, ) {} async requestAvatarGeneration( @@ -66,18 +72,23 @@ export class MediaService { ): Promise { this.logger.debug('[MediaService] Requesting avatar generation.'); - const presenter = new RequestAvatarGenerationPresenter(); - presenter.reset(); - const result = await this.requestAvatarGenerationUseCase.execute({ userId: input.userId, facePhotoData: input.facePhotoData, suitColor: input.suitColor as RacingSuitColor, }); - presenter.present(result); + if (result.isErr()) { + const error = result.unwrapErr(); + return { + success: false, + requestId: '', + avatarUrls: [], + errorMessage: error.details?.message ?? 'Failed to request avatar generation', + }; + } - return presenter.responseModel; + return this.requestAvatarGenerationPresenter.responseModel; } async uploadMedia( @@ -85,69 +96,87 @@ export class MediaService { ): Promise { this.logger.debug('[MediaService] Uploading media.'); - const presenter = new UploadMediaPresenter(); - presenter.reset(); - const result = await this.uploadMediaUseCase.execute({ file: input.file, uploadedBy: input.userId ?? '', - metadata: input.metadata, + metadata: input.metadata || {}, }); - presenter.present(result); + if (result.isErr()) { + const error = result.unwrapErr(); + return { + success: false, + error: error.details?.message ?? 'Upload failed', + }; + } - return presenter.responseModel; + return this.uploadMediaPresenter.responseModel; } async getMedia(mediaId: string): Promise { this.logger.debug(`[MediaService] Getting media: ${mediaId}`); - const presenter = new GetMediaPresenter(); - presenter.reset(); - const result = await this.getMediaUseCase.execute({ mediaId }); - presenter.present(result); - return presenter.responseModel; + if (result.isErr()) { + const error = result.unwrapErr(); + if (error.code === 'MEDIA_NOT_FOUND') { + return null; + } + throw new Error(error.details?.message ?? 'Failed to get media'); + } + + return this.getMediaPresenter.responseModel; } async deleteMedia(mediaId: string): Promise { this.logger.debug(`[MediaService] Deleting media: ${mediaId}`); - const presenter = new DeleteMediaPresenter(); - presenter.reset(); - const result = await this.deleteMediaUseCase.execute({ mediaId }); - presenter.present(result); - return presenter.responseModel; + if (result.isErr()) { + const error = result.unwrapErr(); + return { + success: false, + error: error.details?.message ?? 'Failed to delete media', + }; + } + + return this.deleteMediaPresenter.responseModel; } async getAvatar(driverId: string): Promise { this.logger.debug(`[MediaService] Getting avatar for driver: ${driverId}`); - const presenter = new GetAvatarPresenter(); - presenter.reset(); - const result = await this.getAvatarUseCase.execute({ driverId }); - presenter.present(result); - return presenter.responseModel; + if (result.isErr()) { + const error = result.unwrapErr(); + if (error.code === 'AVATAR_NOT_FOUND') { + return null; + } + throw new Error(error.details?.message ?? 'Failed to get avatar'); + } + + return this.getAvatarPresenter.responseModel; } async updateAvatar(driverId: string, input: UpdateAvatarInput): Promise { this.logger.debug(`[MediaService] Updating avatar for driver: ${driverId}`); - const presenter = new UpdateAvatarPresenter(); - presenter.reset(); - const result = await this.updateAvatarUseCase.execute({ driverId, - mediaUrl: input.mediaUrl, + mediaUrl: input.avatarUrl, }); - presenter.present(result); + if (result.isErr()) { + const error = result.unwrapErr(); + return { + success: false, + error: error.details?.message ?? 'Failed to update avatar', + }; + } - return presenter.responseModel; + return this.updateAvatarPresenter.responseModel; } } diff --git a/apps/api/src/domain/media/presenters/DeleteMediaPresenter.ts b/apps/api/src/domain/media/presenters/DeleteMediaPresenter.ts index d3376da32..c391eeeec 100644 --- a/apps/api/src/domain/media/presenters/DeleteMediaPresenter.ts +++ b/apps/api/src/domain/media/presenters/DeleteMediaPresenter.ts @@ -1,41 +1,19 @@ -import type { Result } from '@core/shared/application/Result'; -import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; -import type { - DeleteMediaResult, - DeleteMediaErrorCode, -} from '@core/media/application/use-cases/DeleteMediaUseCase'; +import type { UseCaseOutputPort } from '@core/shared/application'; +import type { DeleteMediaResult } from '@core/media/application/use-cases/DeleteMediaUseCase'; import type { DeleteMediaOutputDTO } from '../dtos/DeleteMediaOutputDTO'; type DeleteMediaResponseModel = DeleteMediaOutputDTO; -export type DeleteMediaApplicationError = ApplicationErrorCode< - DeleteMediaErrorCode, - { message: string } ->; - -export class DeleteMediaPresenter { +export class DeleteMediaPresenter implements UseCaseOutputPort { private model: DeleteMediaResponseModel | null = null; reset(): void { this.model = null; } - present(result: Result): void { - if (result.isErr()) { - const error = result.unwrapErr(); - - this.model = { - success: false, - error: error.details?.message ?? 'Failed to delete media', - }; - return; - } - - const output = result.unwrap(); - + present(result: DeleteMediaResult): void { this.model = { - success: output.deleted, - error: undefined, + success: result.deleted, }; } diff --git a/apps/api/src/domain/media/presenters/GetAvatarPresenter.ts b/apps/api/src/domain/media/presenters/GetAvatarPresenter.ts index 2a60e74f4..4162a9afa 100644 --- a/apps/api/src/domain/media/presenters/GetAvatarPresenter.ts +++ b/apps/api/src/domain/media/presenters/GetAvatarPresenter.ts @@ -1,41 +1,19 @@ -import type { Result } from '@core/shared/application/Result'; -import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; -import type { - GetAvatarResult, - GetAvatarErrorCode, -} from '@core/media/application/use-cases/GetAvatarUseCase'; +import type { UseCaseOutputPort } from '@core/shared/application'; +import type { GetAvatarResult } from '@core/media/application/use-cases/GetAvatarUseCase'; import type { GetAvatarOutputDTO } from '../dtos/GetAvatarOutputDTO'; export type GetAvatarResponseModel = GetAvatarOutputDTO | null; -export type GetAvatarApplicationError = ApplicationErrorCode< - GetAvatarErrorCode, - { message: string } ->; - -export class GetAvatarPresenter { +export class GetAvatarPresenter implements UseCaseOutputPort { private model: GetAvatarResponseModel | null = null; reset(): void { this.model = null; } - present(result: Result): void { - if (result.isErr()) { - const error = result.unwrapErr(); - - if (error.code === 'AVATAR_NOT_FOUND') { - this.model = null; - return; - } - - throw new Error(error.details?.message ?? 'Failed to get avatar'); - } - - const output = result.unwrap(); - + present(result: GetAvatarResult): void { this.model = { - avatarUrl: output.avatar.mediaUrl, + avatarUrl: result.avatar.mediaUrl, }; } diff --git a/apps/api/src/domain/media/presenters/GetMediaPresenter.ts b/apps/api/src/domain/media/presenters/GetMediaPresenter.ts index 3b9fab169..42cbcf111 100644 --- a/apps/api/src/domain/media/presenters/GetMediaPresenter.ts +++ b/apps/api/src/domain/media/presenters/GetMediaPresenter.ts @@ -1,37 +1,18 @@ -import type { Result } from '@core/shared/application/Result'; -import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; -import type { GetMediaResult, GetMediaErrorCode } from '@core/media/application/use-cases/GetMediaUseCase'; +import type { UseCaseOutputPort } from '@core/shared/application'; +import type { GetMediaResult } from '@core/media/application/use-cases/GetMediaUseCase'; import type { GetMediaOutputDTO } from '../dtos/GetMediaOutputDTO'; export type GetMediaResponseModel = GetMediaOutputDTO | null; -export type GetMediaApplicationError = ApplicationErrorCode< - GetMediaErrorCode, - { message: string } ->; - -export class GetMediaPresenter { +export class GetMediaPresenter implements UseCaseOutputPort { private model: GetMediaResponseModel | null = null; reset(): void { this.model = null; } - present(result: Result): void { - if (result.isErr()) { - const error = result.unwrapErr(); - - if (error.code === 'MEDIA_NOT_FOUND') { - this.model = null; - return; - } - - throw new Error(error.details?.message ?? 'Failed to get media'); - } - - const output = result.unwrap(); - - const media = output.media; + present(result: GetMediaResult): void { + const media = result.media; this.model = { id: media.id, diff --git a/apps/api/src/domain/media/presenters/RequestAvatarGenerationPresenter.ts b/apps/api/src/domain/media/presenters/RequestAvatarGenerationPresenter.ts index eb1583ecd..a59b8e698 100644 --- a/apps/api/src/domain/media/presenters/RequestAvatarGenerationPresenter.ts +++ b/apps/api/src/domain/media/presenters/RequestAvatarGenerationPresenter.ts @@ -1,50 +1,21 @@ -import type { Result } from '@core/shared/application/Result'; -import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; -import type { - RequestAvatarGenerationResult, - RequestAvatarGenerationErrorCode, -} from '@core/media/application/use-cases/RequestAvatarGenerationUseCase'; +import type { UseCaseOutputPort } from '@core/shared/application'; +import type { RequestAvatarGenerationResult } from '@core/media/application/use-cases/RequestAvatarGenerationUseCase'; import type { RequestAvatarGenerationOutputDTO } from '../dtos/RequestAvatarGenerationOutputDTO'; type RequestAvatarGenerationResponseModel = RequestAvatarGenerationOutputDTO; -export type RequestAvatarGenerationApplicationError = ApplicationErrorCode< - RequestAvatarGenerationErrorCode, - { message: string } ->; - -export class RequestAvatarGenerationPresenter { +export class RequestAvatarGenerationPresenter implements UseCaseOutputPort { private model: RequestAvatarGenerationResponseModel | null = null; reset() { this.model = null; } - present( - result: Result< - RequestAvatarGenerationResult, - RequestAvatarGenerationApplicationError - >, - ): void { - if (result.isErr()) { - const error = result.unwrapErr(); - - this.model = { - success: false, - requestId: '', - avatarUrls: [], - errorMessage: error.details?.message ?? 'Failed to request avatar generation', - }; - return; - } - - const output = result.unwrap(); - + present(result: RequestAvatarGenerationResult): void { this.model = { - success: output.status === 'completed', - requestId: output.requestId, - avatarUrls: output.avatarUrls, - errorMessage: undefined, + success: result.status === 'completed', + requestId: result.requestId, + avatarUrls: result.avatarUrls || [], }; } diff --git a/apps/api/src/domain/media/presenters/UpdateAvatarPresenter.ts b/apps/api/src/domain/media/presenters/UpdateAvatarPresenter.ts index 3b93a728b..76ae97453 100644 --- a/apps/api/src/domain/media/presenters/UpdateAvatarPresenter.ts +++ b/apps/api/src/domain/media/presenters/UpdateAvatarPresenter.ts @@ -1,36 +1,19 @@ -import type { Result } from '@core/shared/application/Result'; -import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; -import type { - UpdateAvatarResult, - UpdateAvatarErrorCode, -} from '@core/media/application/use-cases/UpdateAvatarUseCase'; +import type { UseCaseOutputPort } from '@core/shared/application'; +import type { UpdateAvatarResult } from '@core/media/application/use-cases/UpdateAvatarUseCase'; import type { UpdateAvatarOutputDTO } from '../dtos/UpdateAvatarOutputDTO'; type UpdateAvatarResponseModel = UpdateAvatarOutputDTO; -export type UpdateAvatarApplicationError = ApplicationErrorCode< - UpdateAvatarErrorCode, - { message: string } ->; - -export class UpdateAvatarPresenter { +export class UpdateAvatarPresenter implements UseCaseOutputPort { private model: UpdateAvatarResponseModel | null = null; reset(): void { this.model = null; } - present(result: Result): void { - if (result.isErr()) { - const error = result.unwrapErr(); - throw new Error(error.details?.message ?? 'Failed to update avatar'); - } - - const output = result.unwrap(); - + present(result: UpdateAvatarResult): void { this.model = { success: true, - error: undefined, }; } diff --git a/apps/api/src/domain/media/presenters/UploadMediaPresenter.ts b/apps/api/src/domain/media/presenters/UploadMediaPresenter.ts index 93325e7b6..f5836edc0 100644 --- a/apps/api/src/domain/media/presenters/UploadMediaPresenter.ts +++ b/apps/api/src/domain/media/presenters/UploadMediaPresenter.ts @@ -1,43 +1,21 @@ -import type { Result } from '@core/shared/application/Result'; -import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; -import type { - UploadMediaResult, - UploadMediaErrorCode, -} from '@core/media/application/use-cases/UploadMediaUseCase'; +import type { UseCaseOutputPort } from '@core/shared/application'; +import type { UploadMediaResult } from '@core/media/application/use-cases/UploadMediaUseCase'; import type { UploadMediaOutputDTO } from '../dtos/UploadMediaOutputDTO'; type UploadMediaResponseModel = UploadMediaOutputDTO; -export type UploadMediaApplicationError = ApplicationErrorCode< - UploadMediaErrorCode, - { message: string } ->; - -export class UploadMediaPresenter { +export class UploadMediaPresenter implements UseCaseOutputPort { private model: UploadMediaResponseModel | null = null; reset(): void { this.model = null; } - present(result: Result): void { - if (result.isErr()) { - const error = result.unwrapErr(); - - this.model = { - success: false, - error: error.details?.message ?? 'Upload failed', - }; - return; - } - - const output = result.unwrap(); - + present(result: UploadMediaResult): void { this.model = { success: true, - mediaId: output.mediaId, - url: output.url, - error: undefined, + mediaId: result.mediaId, + url: result.url, }; } diff --git a/apps/api/src/domain/payments/PaymentsController.ts b/apps/api/src/domain/payments/PaymentsController.ts index 80846396f..c30ad04c9 100644 --- a/apps/api/src/domain/payments/PaymentsController.ts +++ b/apps/api/src/domain/payments/PaymentsController.ts @@ -12,8 +12,7 @@ export class PaymentsController { @ApiOperation({ summary: 'Get payments based on filters' }) @ApiResponse({ status: 200, description: 'List of payments', type: GetPaymentsOutput }) async getPayments(@Query() query: GetPaymentsQuery): Promise { - const presenter = await this.paymentsService.getPayments(query); - return presenter.viewModel; + return this.paymentsService.getPayments(query); } @Post() @@ -21,16 +20,14 @@ export class PaymentsController { @ApiOperation({ summary: 'Create a new payment' }) @ApiResponse({ status: 201, description: 'Payment created', type: CreatePaymentOutput }) async createPayment(@Body() input: CreatePaymentInput): Promise { - const presenter = await this.paymentsService.createPayment(input); - return presenter.viewModel; + return this.paymentsService.createPayment(input); } @Patch('status') @ApiOperation({ summary: 'Update the status of a payment' }) @ApiResponse({ status: 200, description: 'Payment status updated', type: UpdatePaymentStatusOutput }) async updatePaymentStatus(@Body() input: UpdatePaymentStatusInput): Promise { - const presenter = await this.paymentsService.updatePaymentStatus(input); - return presenter.viewModel; + return this.paymentsService.updatePaymentStatus(input); } @Get('membership-fees') diff --git a/apps/api/src/domain/payments/PaymentsProviders.ts b/apps/api/src/domain/payments/PaymentsProviders.ts index 527c6d3e8..ee0b7b1ad 100644 --- a/apps/api/src/domain/payments/PaymentsProviders.ts +++ b/apps/api/src/domain/payments/PaymentsProviders.ts @@ -6,7 +6,7 @@ import type { IPaymentRepository } from '@core/payments/domain/repositories/IPay import type { IMembershipFeeRepository, IMemberPaymentRepository } from '@core/payments/domain/repositories/IMembershipFeeRepository'; import type { IPrizeRepository } from '@core/payments/domain/repositories/IPrizeRepository'; import type { IWalletRepository, ITransactionRepository } from '@core/payments/domain/repositories/IWalletRepository'; -import type { Logger } from '@core/shared/application/Logger'; +import type { Logger, UseCaseOutputPort } from '@core/shared/application'; // Import use cases import { GetPaymentsUseCase } from '@core/payments/application/use-cases/GetPaymentsUseCase'; @@ -29,6 +29,20 @@ import { InMemoryPrizeRepository } from '@adapters/payments/persistence/inmemory import { InMemoryWalletRepository, InMemoryTransactionRepository } from '@adapters/payments/persistence/inmemory/InMemoryWalletRepository'; import { ConsoleLogger } from '@adapters/logging/ConsoleLogger'; +// Presenters +import { GetPaymentsPresenter } from './presenters/GetPaymentsPresenter'; +import { CreatePaymentPresenter } from './presenters/CreatePaymentPresenter'; +import { UpdatePaymentStatusPresenter } from './presenters/UpdatePaymentStatusPresenter'; +import { GetMembershipFeesPresenter } from './presenters/GetMembershipFeesPresenter'; +import { UpsertMembershipFeePresenter } from './presenters/UpsertMembershipFeePresenter'; +import { UpdateMemberPaymentPresenter } from './presenters/UpdateMemberPaymentPresenter'; +import { GetPrizesPresenter } from './presenters/GetPrizesPresenter'; +import { CreatePrizePresenter } from './presenters/CreatePrizePresenter'; +import { AwardPrizePresenter } from './presenters/AwardPrizePresenter'; +import { DeletePrizePresenter } from './presenters/DeletePrizePresenter'; +import { GetWalletPresenter } from './presenters/GetWalletPresenter'; +import { ProcessWalletTransactionPresenter } from './presenters/ProcessWalletTransactionPresenter'; + // Repository injection tokens export const PAYMENT_REPOSITORY_TOKEN = 'IPaymentRepository'; export const MEMBERSHIP_FEE_REPOSITORY_TOKEN = 'IMembershipFeeRepository'; @@ -52,9 +66,87 @@ export const DELETE_PRIZE_USE_CASE_TOKEN = 'DeletePrizeUseCase'; export const GET_WALLET_USE_CASE_TOKEN = 'GetWalletUseCase'; export const PROCESS_WALLET_TRANSACTION_USE_CASE_TOKEN = 'ProcessWalletTransactionUseCase'; +// Output port tokens +export const GET_PAYMENTS_OUTPUT_PORT_TOKEN = 'GetPaymentsOutputPort_TOKEN'; +export const CREATE_PAYMENT_OUTPUT_PORT_TOKEN = 'CreatePaymentOutputPort_TOKEN'; +export const UPDATE_PAYMENT_STATUS_OUTPUT_PORT_TOKEN = 'UpdatePaymentStatusOutputPort_TOKEN'; +export const GET_MEMBERSHIP_FEES_OUTPUT_PORT_TOKEN = 'GetMembershipFeesOutputPort_TOKEN'; +export const UPSERT_MEMBERSHIP_FEE_OUTPUT_PORT_TOKEN = 'UpsertMembershipFeeOutputPort_TOKEN'; +export const UPDATE_MEMBER_PAYMENT_OUTPUT_PORT_TOKEN = 'UpdateMemberPaymentOutputPort_TOKEN'; +export const GET_PRIZES_OUTPUT_PORT_TOKEN = 'GetPrizesOutputPort_TOKEN'; +export const CREATE_PRIZE_OUTPUT_PORT_TOKEN = 'CreatePrizeOutputPort_TOKEN'; +export const AWARD_PRIZE_OUTPUT_PORT_TOKEN = 'AwardPrizeOutputPort_TOKEN'; +export const DELETE_PRIZE_OUTPUT_PORT_TOKEN = 'DeletePrizeOutputPort_TOKEN'; +export const GET_WALLET_OUTPUT_PORT_TOKEN = 'GetWalletOutputPort_TOKEN'; +export const PROCESS_WALLET_TRANSACTION_OUTPUT_PORT_TOKEN = 'ProcessWalletTransactionOutputPort_TOKEN'; + export const PaymentsProviders: Provider[] = [ PaymentsService, + // Presenters + GetPaymentsPresenter, + CreatePaymentPresenter, + UpdatePaymentStatusPresenter, + GetMembershipFeesPresenter, + UpsertMembershipFeePresenter, + UpdateMemberPaymentPresenter, + GetPrizesPresenter, + CreatePrizePresenter, + AwardPrizePresenter, + DeletePrizePresenter, + GetWalletPresenter, + ProcessWalletTransactionPresenter, + + // Output ports + { + provide: GET_PAYMENTS_OUTPUT_PORT_TOKEN, + useExisting: GetPaymentsPresenter, + }, + { + provide: CREATE_PAYMENT_OUTPUT_PORT_TOKEN, + useExisting: CreatePaymentPresenter, + }, + { + provide: UPDATE_PAYMENT_STATUS_OUTPUT_PORT_TOKEN, + useExisting: UpdatePaymentStatusPresenter, + }, + { + provide: GET_MEMBERSHIP_FEES_OUTPUT_PORT_TOKEN, + useExisting: GetMembershipFeesPresenter, + }, + { + provide: UPSERT_MEMBERSHIP_FEE_OUTPUT_PORT_TOKEN, + useExisting: UpsertMembershipFeePresenter, + }, + { + provide: UPDATE_MEMBER_PAYMENT_OUTPUT_PORT_TOKEN, + useExisting: UpdateMemberPaymentPresenter, + }, + { + provide: GET_PRIZES_OUTPUT_PORT_TOKEN, + useExisting: GetPrizesPresenter, + }, + { + provide: CREATE_PRIZE_OUTPUT_PORT_TOKEN, + useExisting: CreatePrizePresenter, + }, + { + provide: AWARD_PRIZE_OUTPUT_PORT_TOKEN, + useExisting: AwardPrizePresenter, + }, + { + provide: DELETE_PRIZE_OUTPUT_PORT_TOKEN, + useExisting: DeletePrizePresenter, + }, + { + provide: GET_WALLET_OUTPUT_PORT_TOKEN, + useExisting: GetWalletPresenter, + }, + { + provide: PROCESS_WALLET_TRANSACTION_OUTPUT_PORT_TOKEN, + useExisting: ProcessWalletTransactionPresenter, + }, + // Logger { provide: LOGGER_TOKEN, @@ -96,66 +188,66 @@ export const PaymentsProviders: Provider[] = [ // Use cases (use cases receive repositories, services receive use cases) { provide: GET_PAYMENTS_USE_CASE_TOKEN, - useFactory: (paymentRepo: IPaymentRepository) => new GetPaymentsUseCase(paymentRepo), - inject: [PAYMENT_REPOSITORY_TOKEN], + useFactory: (paymentRepo: IPaymentRepository, output: UseCaseOutputPort) => new GetPaymentsUseCase(paymentRepo, output), + inject: [PAYMENT_REPOSITORY_TOKEN, GET_PAYMENTS_OUTPUT_PORT_TOKEN], }, { provide: CREATE_PAYMENT_USE_CASE_TOKEN, - useFactory: (paymentRepo: IPaymentRepository) => new CreatePaymentUseCase(paymentRepo), - inject: [PAYMENT_REPOSITORY_TOKEN], + useFactory: (paymentRepo: IPaymentRepository, output: UseCaseOutputPort) => new CreatePaymentUseCase(paymentRepo, output), + inject: [PAYMENT_REPOSITORY_TOKEN, CREATE_PAYMENT_OUTPUT_PORT_TOKEN], }, { provide: UPDATE_PAYMENT_STATUS_USE_CASE_TOKEN, - useFactory: (paymentRepo: IPaymentRepository) => new UpdatePaymentStatusUseCase(paymentRepo), - inject: [PAYMENT_REPOSITORY_TOKEN], + useFactory: (paymentRepo: IPaymentRepository, output: UseCaseOutputPort) => new UpdatePaymentStatusUseCase(paymentRepo, output), + inject: [PAYMENT_REPOSITORY_TOKEN, UPDATE_PAYMENT_STATUS_OUTPUT_PORT_TOKEN], }, { provide: GET_MEMBERSHIP_FEES_USE_CASE_TOKEN, - useFactory: (membershipFeeRepo: IMembershipFeeRepository, memberPaymentRepo: IMemberPaymentRepository) => - new GetMembershipFeesUseCase(membershipFeeRepo, memberPaymentRepo), - inject: [MEMBERSHIP_FEE_REPOSITORY_TOKEN, MEMBER_PAYMENT_REPOSITORY_TOKEN], + useFactory: (membershipFeeRepo: IMembershipFeeRepository, memberPaymentRepo: IMemberPaymentRepository, output: UseCaseOutputPort) => + new GetMembershipFeesUseCase(membershipFeeRepo, memberPaymentRepo, output), + inject: [MEMBERSHIP_FEE_REPOSITORY_TOKEN, MEMBER_PAYMENT_REPOSITORY_TOKEN, GET_MEMBERSHIP_FEES_OUTPUT_PORT_TOKEN], }, { provide: UPSERT_MEMBERSHIP_FEE_USE_CASE_TOKEN, - useFactory: (membershipFeeRepo: IMembershipFeeRepository) => new UpsertMembershipFeeUseCase(membershipFeeRepo), - inject: [MEMBERSHIP_FEE_REPOSITORY_TOKEN], + useFactory: (membershipFeeRepo: IMembershipFeeRepository, output: UseCaseOutputPort) => new UpsertMembershipFeeUseCase(membershipFeeRepo, output), + inject: [MEMBERSHIP_FEE_REPOSITORY_TOKEN, UPSERT_MEMBERSHIP_FEE_OUTPUT_PORT_TOKEN], }, { provide: UPDATE_MEMBER_PAYMENT_USE_CASE_TOKEN, - useFactory: (membershipFeeRepo: IMembershipFeeRepository, memberPaymentRepo: IMemberPaymentRepository) => - new UpdateMemberPaymentUseCase(membershipFeeRepo, memberPaymentRepo), - inject: [MEMBERSHIP_FEE_REPOSITORY_TOKEN, MEMBER_PAYMENT_REPOSITORY_TOKEN], + useFactory: (membershipFeeRepo: IMembershipFeeRepository, memberPaymentRepo: IMemberPaymentRepository, output: UseCaseOutputPort) => + new UpdateMemberPaymentUseCase(membershipFeeRepo, memberPaymentRepo, output), + inject: [MEMBERSHIP_FEE_REPOSITORY_TOKEN, MEMBER_PAYMENT_REPOSITORY_TOKEN, UPDATE_MEMBER_PAYMENT_OUTPUT_PORT_TOKEN], }, { provide: GET_PRIZES_USE_CASE_TOKEN, - useFactory: (prizeRepo: IPrizeRepository) => new GetPrizesUseCase(prizeRepo), - inject: [PRIZE_REPOSITORY_TOKEN], + useFactory: (prizeRepo: IPrizeRepository, output: UseCaseOutputPort) => new GetPrizesUseCase(prizeRepo, output), + inject: [PRIZE_REPOSITORY_TOKEN, GET_PRIZES_OUTPUT_PORT_TOKEN], }, { provide: CREATE_PRIZE_USE_CASE_TOKEN, - useFactory: (prizeRepo: IPrizeRepository) => new CreatePrizeUseCase(prizeRepo), - inject: [PRIZE_REPOSITORY_TOKEN], + useFactory: (prizeRepo: IPrizeRepository, output: UseCaseOutputPort) => new CreatePrizeUseCase(prizeRepo, output), + inject: [PRIZE_REPOSITORY_TOKEN, CREATE_PRIZE_OUTPUT_PORT_TOKEN], }, { provide: AWARD_PRIZE_USE_CASE_TOKEN, - useFactory: (prizeRepo: IPrizeRepository) => new AwardPrizeUseCase(prizeRepo), - inject: [PRIZE_REPOSITORY_TOKEN], + useFactory: (prizeRepo: IPrizeRepository, output: UseCaseOutputPort) => new AwardPrizeUseCase(prizeRepo, output), + inject: [PRIZE_REPOSITORY_TOKEN, AWARD_PRIZE_OUTPUT_PORT_TOKEN], }, { provide: DELETE_PRIZE_USE_CASE_TOKEN, - useFactory: (prizeRepo: IPrizeRepository) => new DeletePrizeUseCase(prizeRepo), - inject: [PRIZE_REPOSITORY_TOKEN], + useFactory: (prizeRepo: IPrizeRepository, output: UseCaseOutputPort) => new DeletePrizeUseCase(prizeRepo, output), + inject: [PRIZE_REPOSITORY_TOKEN, DELETE_PRIZE_OUTPUT_PORT_TOKEN], }, { provide: GET_WALLET_USE_CASE_TOKEN, - useFactory: (walletRepo: IWalletRepository, transactionRepo: ITransactionRepository) => - new GetWalletUseCase(walletRepo, transactionRepo), - inject: [WALLET_REPOSITORY_TOKEN, TRANSACTION_REPOSITORY_TOKEN], + useFactory: (walletRepo: IWalletRepository, transactionRepo: ITransactionRepository, output: UseCaseOutputPort) => + new GetWalletUseCase(walletRepo, transactionRepo, output), + inject: [WALLET_REPOSITORY_TOKEN, TRANSACTION_REPOSITORY_TOKEN, GET_WALLET_OUTPUT_PORT_TOKEN], }, { provide: PROCESS_WALLET_TRANSACTION_USE_CASE_TOKEN, - useFactory: (walletRepo: IWalletRepository, transactionRepo: ITransactionRepository) => - new ProcessWalletTransactionUseCase(walletRepo, transactionRepo), - inject: [WALLET_REPOSITORY_TOKEN, TRANSACTION_REPOSITORY_TOKEN], + useFactory: (walletRepo: IWalletRepository, transactionRepo: ITransactionRepository, output: UseCaseOutputPort) => + new ProcessWalletTransactionUseCase(walletRepo, transactionRepo, output), + inject: [WALLET_REPOSITORY_TOKEN, TRANSACTION_REPOSITORY_TOKEN, PROCESS_WALLET_TRANSACTION_OUTPUT_PORT_TOKEN], }, ]; diff --git a/apps/api/src/domain/payments/PaymentsService.ts b/apps/api/src/domain/payments/PaymentsService.ts index 9ed48ca08..f70a239e0 100644 --- a/apps/api/src/domain/payments/PaymentsService.ts +++ b/apps/api/src/domain/payments/PaymentsService.ts @@ -90,54 +90,78 @@ export class PaymentsService { @Inject(GET_WALLET_USE_CASE_TOKEN) private readonly getWalletUseCase: GetWalletUseCase, @Inject(PROCESS_WALLET_TRANSACTION_USE_CASE_TOKEN) private readonly processWalletTransactionUseCase: ProcessWalletTransactionUseCase, @Inject(LOGGER_TOKEN) private readonly logger: Logger, + private readonly getPaymentsPresenter: GetPaymentsPresenter, + private readonly createPaymentPresenter: CreatePaymentPresenter, + private readonly updatePaymentStatusPresenter: UpdatePaymentStatusPresenter, + private readonly getMembershipFeesPresenter: GetMembershipFeesPresenter, + private readonly upsertMembershipFeePresenter: UpsertMembershipFeePresenter, + private readonly updateMemberPaymentPresenter: UpdateMemberPaymentPresenter, + private readonly getPrizesPresenter: GetPrizesPresenter, + private readonly createPrizePresenter: CreatePrizePresenter, + private readonly awardPrizePresenter: AwardPrizePresenter, + private readonly deletePrizePresenter: DeletePrizePresenter, + private readonly getWalletPresenter: GetWalletPresenter, + private readonly processWalletTransactionPresenter: ProcessWalletTransactionPresenter, ) {} - async getPayments(query: GetPaymentsQuery): Promise { + async getPayments(query: GetPaymentsQuery): Promise { this.logger.debug('[PaymentsService] Getting payments', { query }); - const presenter = new GetPaymentsPresenter(); - await this.getPaymentsUseCase.execute(query, presenter); - return presenter; + const result = await this.getPaymentsUseCase.execute(query); + if (result.isErr()) { + throw new Error(result.unwrapErr().details?.message ?? 'Failed to get payments'); + } + return this.getPaymentsPresenter.getResponseModel(); } - async createPayment(input: CreatePaymentInput): Promise { + async createPayment(input: CreatePaymentInput): Promise { this.logger.debug('[PaymentsService] Creating payment', { input }); - const presenter = new CreatePaymentPresenter(); - await this.createPaymentUseCase.execute(input, presenter); - return presenter; + const result = await this.createPaymentUseCase.execute(input); + if (result.isErr()) { + throw new Error(result.unwrapErr().details?.message ?? 'Failed to create payment'); + } + return this.createPaymentPresenter.getResponseModel(); } - async updatePaymentStatus(input: UpdatePaymentStatusInput): Promise { + async updatePaymentStatus(input: UpdatePaymentStatusInput): Promise { this.logger.debug('[PaymentsService] Updating payment status', { input }); - const presenter = new UpdatePaymentStatusPresenter(); - await this.updatePaymentStatusUseCase.execute(input, presenter); - return presenter; + const result = await this.updatePaymentStatusUseCase.execute(input); + if (result.isErr()) { + throw new Error(result.unwrapErr().details?.message ?? 'Failed to update payment status'); + } + return this.updatePaymentStatusPresenter.getResponseModel(); } - async getMembershipFees(query: GetMembershipFeesQuery): Promise { + async getMembershipFees(query: GetMembershipFeesQuery): Promise { this.logger.debug('[PaymentsService] Getting membership fees', { query }); - const presenter = new GetMembershipFeesPresenter(); - await this.getMembershipFeesUseCase.execute(query, presenter); - return presenter; + const result = await this.getMembershipFeesUseCase.execute(query); + if (result.isErr()) { + throw new Error(result.unwrapErr().details?.message ?? 'Failed to get membership fees'); + } + return this.getMembershipFeesPresenter.getResponseModel(); } - async upsertMembershipFee(input: UpsertMembershipFeeInput): Promise { + async upsertMembershipFee(input: UpsertMembershipFeeInput): Promise { this.logger.debug('[PaymentsService] Upserting membership fee', { input }); - const presenter = new UpsertMembershipFeePresenter(); - await this.upsertMembershipFeeUseCase.execute(input, presenter); - return presenter; + const result = await this.upsertMembershipFeeUseCase.execute(input); + if (result.isErr()) { + throw new Error(result.unwrapErr().details?.message ?? 'Failed to upsert membership fee'); + } + return this.upsertMembershipFeePresenter.getResponseModel(); } - async updateMemberPayment(input: UpdateMemberPaymentInput): Promise { + async updateMemberPayment(input: UpdateMemberPaymentInput): Promise { this.logger.debug('[PaymentsService] Updating member payment', { input }); - const presenter = new UpdateMemberPaymentPresenter(); - await this.updateMemberPaymentUseCase.execute(input, presenter); - return presenter; + const result = await this.updateMemberPaymentUseCase.execute(input); + if (result.isErr()) { + throw new Error(result.unwrapErr().details?.message ?? 'Failed to update member payment'); + } + return this.updateMemberPaymentPresenter.getResponseModel(); } async getPrizes(query: GetPrizesQuery): Promise { diff --git a/apps/api/src/domain/payments/presenters/CreatePaymentPresenter.ts b/apps/api/src/domain/payments/presenters/CreatePaymentPresenter.ts index 87246ed03..0f8f8dcdb 100644 --- a/apps/api/src/domain/payments/presenters/CreatePaymentPresenter.ts +++ b/apps/api/src/domain/payments/presenters/CreatePaymentPresenter.ts @@ -1,25 +1,34 @@ -import type { - ICreatePaymentPresenter, - CreatePaymentResultDTO, - CreatePaymentViewModel, -} from '@core/payments/application/presenters/ICreatePaymentPresenter'; +import type { UseCaseOutputPort } from '@core/shared/application'; +import type { CreatePaymentResult } from '@core/payments/application/use-cases/CreatePaymentUseCase'; +import type { CreatePaymentOutput } from '../dtos/PaymentsDto'; -export class CreatePaymentPresenter implements ICreatePaymentPresenter { - private responseModel: CreatePaymentViewModel | null = null; +export class CreatePaymentPresenter implements UseCaseOutputPort { + private responseModel: CreatePaymentOutput | null = null; reset() { this.responseModel = null; } - present(dto: CreatePaymentResultDTO) { - this.responseModel = dto; + present(result: CreatePaymentResult): void { + this.responseModel = { + payment: { + id: result.payment.id, + type: result.payment.type, + amount: result.payment.amount, + platformFee: result.payment.platformFee, + netAmount: result.payment.netAmount, + payerId: result.payment.payerId, + payerType: result.payment.payerType, + leagueId: result.payment.leagueId, + ...(result.payment.seasonId !== undefined ? { seasonId: result.payment.seasonId } : {}), + status: result.payment.status, + createdAt: result.payment.createdAt, + ...(result.payment.completedAt !== undefined ? { completedAt: result.payment.completedAt } : {}), + }, + }; } - getResponseModel(): CreatePaymentViewModel | null { - return this.responseModel; - } - - get responseModel(): CreatePaymentViewModel { + getResponseModel(): CreatePaymentOutput { if (!this.responseModel) throw new Error('Presenter not presented'); return this.responseModel; } diff --git a/apps/api/src/domain/payments/presenters/GetPaymentsPresenter.ts b/apps/api/src/domain/payments/presenters/GetPaymentsPresenter.ts index 786c52b7b..1271e9ed6 100644 --- a/apps/api/src/domain/payments/presenters/GetPaymentsPresenter.ts +++ b/apps/api/src/domain/payments/presenters/GetPaymentsPresenter.ts @@ -1,26 +1,35 @@ -import type { - IGetPaymentsPresenter, - GetPaymentsResultDTO, - GetPaymentsViewModel, -} from '@core/payments/application/presenters/IGetPaymentsPresenter'; +import type { UseCaseOutputPort } from '@core/shared/application'; +import type { GetPaymentsResult } from '@core/payments/application/use-cases/GetPaymentsUseCase'; +import type { GetPaymentsOutput } from '../dtos/PaymentsDto'; -export class GetPaymentsPresenter implements IGetPaymentsPresenter { - private result: GetPaymentsViewModel | null = null; +export class GetPaymentsPresenter implements UseCaseOutputPort { + private responseModel: GetPaymentsOutput | null = null; reset() { - this.result = null; + this.responseModel = null; } - present(dto: GetPaymentsResultDTO) { - this.result = dto; + present(result: GetPaymentsResult): void { + this.responseModel = { + payments: result.payments.map(payment => ({ + id: payment.id, + type: payment.type, + amount: payment.amount, + platformFee: payment.platformFee, + netAmount: payment.netAmount, + payerId: payment.payerId, + payerType: payment.payerType, + leagueId: payment.leagueId, + ...(payment.seasonId !== undefined ? { seasonId: payment.seasonId } : {}), + status: payment.status, + createdAt: payment.createdAt, + ...(payment.completedAt !== undefined ? { completedAt: payment.completedAt } : {}), + })), + }; } - getViewModel(): GetPaymentsViewModel | null { - return this.result; - } - - get viewModel(): GetPaymentsViewModel { - if (!this.result) throw new Error('Presenter not presented'); - return this.result; + getResponseModel(): GetPaymentsOutput { + if (!this.responseModel) throw new Error('Presenter not presented'); + return this.responseModel; } } \ No newline at end of file diff --git a/apps/api/src/domain/payments/presenters/UpdatePaymentStatusPresenter.ts b/apps/api/src/domain/payments/presenters/UpdatePaymentStatusPresenter.ts index ff6699902..55d74beb7 100644 --- a/apps/api/src/domain/payments/presenters/UpdatePaymentStatusPresenter.ts +++ b/apps/api/src/domain/payments/presenters/UpdatePaymentStatusPresenter.ts @@ -1,26 +1,35 @@ -import type { - IUpdatePaymentStatusPresenter, - UpdatePaymentStatusResultDTO, - UpdatePaymentStatusViewModel, -} from '@core/payments/application/presenters/IUpdatePaymentStatusPresenter'; +import type { UseCaseOutputPort } from '@core/shared/application'; +import type { UpdatePaymentStatusResult } from '@core/payments/application/use-cases/UpdatePaymentStatusUseCase'; +import type { UpdatePaymentStatusOutput } from '../dtos/PaymentsDto'; -export class UpdatePaymentStatusPresenter implements IUpdatePaymentStatusPresenter { - private result: UpdatePaymentStatusViewModel | null = null; +export class UpdatePaymentStatusPresenter implements UseCaseOutputPort { + private responseModel: UpdatePaymentStatusOutput | null = null; reset() { - this.result = null; + this.responseModel = null; } - present(dto: UpdatePaymentStatusResultDTO) { - this.result = dto; + present(result: UpdatePaymentStatusResult): void { + this.responseModel = { + payment: { + id: result.payment.id, + type: result.payment.type, + amount: result.payment.amount, + platformFee: result.payment.platformFee, + netAmount: result.payment.netAmount, + payerId: result.payment.payerId, + payerType: result.payment.payerType, + leagueId: result.payment.leagueId, + ...(result.payment.seasonId !== undefined ? { seasonId: result.payment.seasonId } : {}), + status: result.payment.status, + createdAt: result.payment.createdAt, + ...(result.payment.completedAt !== undefined ? { completedAt: result.payment.completedAt } : {}), + }, + }; } - getViewModel(): UpdatePaymentStatusViewModel | null { - return this.result; - } - - get viewModel(): UpdatePaymentStatusViewModel { - if (!this.result) throw new Error('Presenter not presented'); - return this.result; + getResponseModel(): UpdatePaymentStatusOutput { + if (!this.responseModel) throw new Error('Presenter not presented'); + return this.responseModel; } } \ No newline at end of file diff --git a/apps/api/src/domain/protests/presenters/ReviewProtestPresenter.ts b/apps/api/src/domain/protests/presenters/ReviewProtestPresenter.ts index 5c237c9df..ba9f2ee30 100644 --- a/apps/api/src/domain/protests/presenters/ReviewProtestPresenter.ts +++ b/apps/api/src/domain/protests/presenters/ReviewProtestPresenter.ts @@ -1,8 +1,5 @@ -import type { Result } from '@core/shared/application/Result'; -import type { - ReviewProtestResult, - ReviewProtestApplicationError, -} from '@core/racing/application/use-cases/ReviewProtestUseCase'; +import type { UseCaseOutputPort } from '@core/shared/application'; +import type { ReviewProtestResult } from '@core/racing/application/use-cases/ReviewProtestUseCase'; export interface ReviewProtestResponseDTO { success: boolean; @@ -13,34 +10,18 @@ export interface ReviewProtestResponseDTO { decision?: 'uphold' | 'dismiss'; } -export class ReviewProtestPresenter { +export class ReviewProtestPresenter implements UseCaseOutputPort { private model: ReviewProtestResponseDTO | null = null; reset(): void { this.model = null; } - present( - result: Result, - ): void { - if (result.isErr()) { - const error = result.unwrapErr(); - - this.model = { - success: false, - errorCode: error.code, - message: error.details?.message, - }; - return; - } - - const value = result.unwrap(); - + present(result: ReviewProtestResult): void { this.model = { success: true, - protestId: value.protestId, - stewardId: value.stewardId, - decision: value.decision, + protestId: result.protestId, + decision: result.status === 'upheld' ? 'uphold' : 'dismiss', }; } diff --git a/apps/api/src/domain/race/RaceService.ts b/apps/api/src/domain/race/RaceService.ts index 3e298b14a..08044c77b 100644 --- a/apps/api/src/domain/race/RaceService.ts +++ b/apps/api/src/domain/race/RaceService.ts @@ -96,11 +96,14 @@ export class RaceService { async getAllRaces(): Promise { this.logger.debug('[RaceService] Fetching all races.'); + const presenter = new GetAllRacesPresenter(); + this.getAllRacesUseCase.setOutput(presenter); + const result = await this.getAllRacesUseCase.execute({}); - const presenter = new GetAllRacesPresenter(); - presenter.reset(); - presenter.present(result); + if (result.isErr()) { + throw new Error(result.unwrapErr().code); + } return presenter; } diff --git a/apps/api/src/domain/race/presenters/GetAllRacesPresenter.ts b/apps/api/src/domain/race/presenters/GetAllRacesPresenter.ts index b68eab575..5b5bee1ef 100644 --- a/apps/api/src/domain/race/presenters/GetAllRacesPresenter.ts +++ b/apps/api/src/domain/race/presenters/GetAllRacesPresenter.ts @@ -1,37 +1,17 @@ -import type { Result } from '@core/shared/application/Result'; -import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; -import type { - GetAllRacesResult, - GetAllRacesErrorCode, -} from '@core/racing/application/use-cases/GetAllRacesUseCase'; +import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort'; +import type { GetAllRacesResult } from '@core/racing/application/use-cases/GetAllRacesUseCase'; import type { AllRacesPageDTO } from '../dtos/AllRacesPageDTO'; export type GetAllRacesResponseModel = AllRacesPageDTO; -export type GetAllRacesApplicationError = ApplicationErrorCode< - GetAllRacesErrorCode, - { message: string } ->; - -export class GetAllRacesPresenter { +export class GetAllRacesPresenter implements UseCaseOutputPort { private model: GetAllRacesResponseModel | null = null; - reset(): void { - this.model = null; - } - - present(result: Result): void { - if (result.isErr()) { - const error = result.unwrapErr(); - throw new Error(error.details?.message ?? 'Failed to get all races'); - } - - const output = result.unwrap(); - + present(result: GetAllRacesResult): void { const leagueMap = new Map(); const uniqueLeagues = new Map(); - for (const league of output.leagues) { + for (const league of result.leagues) { const id = league.id.toString(); const name = league.name.toString(); leagueMap.set(id, name); @@ -39,7 +19,7 @@ export class GetAllRacesPresenter { } this.model = { - races: output.races.map(race => ({ + races: result.races.map(race => ({ id: race.id, track: race.track, car: race.car, diff --git a/core/identity/application/use-cases/LogoutUseCase.ts b/core/identity/application/use-cases/LogoutUseCase.ts index fb966e6c3..c5eb592b9 100644 --- a/core/identity/application/use-cases/LogoutUseCase.ts +++ b/core/identity/application/use-cases/LogoutUseCase.ts @@ -1,7 +1,7 @@ import type { IdentitySessionPort } from '../ports/IdentitySessionPort'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; -import type { UseCaseOutputPort, Logger } from '@core/shared/application'; +import type { UseCaseOutputPort, Logger, UseCase } from '@core/shared/application'; export type LogoutInput = {}; @@ -13,7 +13,7 @@ export type LogoutErrorCode = 'REPOSITORY_ERROR'; export type LogoutApplicationError = ApplicationErrorCode; -export class LogoutUseCase { +export class LogoutUseCase implements UseCase { private readonly sessionPort: IdentitySessionPort; constructor( @@ -24,7 +24,7 @@ export class LogoutUseCase { this.sessionPort = sessionPort; } - async execute(): Promise> { + async execute(input: LogoutInput): Promise> { try { await this.sessionPort.clearSession(); diff --git a/core/racing/application/use-cases/GetAllRacesUseCase.test.ts b/core/racing/application/use-cases/GetAllRacesUseCase.test.ts index 0cb43677e..1db794420 100644 --- a/core/racing/application/use-cases/GetAllRacesUseCase.test.ts +++ b/core/racing/application/use-cases/GetAllRacesUseCase.test.ts @@ -58,8 +58,8 @@ describe('GetAllRacesUseCase', () => { mockRaceRepo, mockLeagueRepo, mockLogger, - output, ); + useCase.setOutput(output); const race1 = { id: 'race1', @@ -100,8 +100,8 @@ describe('GetAllRacesUseCase', () => { mockRaceRepo, mockLeagueRepo, mockLogger, - output, ); + useCase.setOutput(output); mockRaceFindAll.mockResolvedValue([]); mockLeagueFindAll.mockResolvedValue([]); diff --git a/core/racing/application/use-cases/GetAllRacesUseCase.ts b/core/racing/application/use-cases/GetAllRacesUseCase.ts index 29b0c1900..1dfe48b41 100644 --- a/core/racing/application/use-cases/GetAllRacesUseCase.ts +++ b/core/racing/application/use-cases/GetAllRacesUseCase.ts @@ -18,13 +18,18 @@ export interface GetAllRacesResult { export type GetAllRacesErrorCode = 'REPOSITORY_ERROR'; export class GetAllRacesUseCase { + private output: UseCaseOutputPort | null = null; + constructor( private readonly raceRepository: IRaceRepository, private readonly leagueRepository: ILeagueRepository, private readonly logger: Logger, - private readonly output: UseCaseOutputPort, ) {} + setOutput(output: UseCaseOutputPort) { + this.output = output; + } + async execute( _input: GetAllRacesInput, ): Promise>> { @@ -40,6 +45,9 @@ export class GetAllRacesUseCase { }; this.logger.debug('Successfully retrieved all races.'); + if (!this.output) { + throw new Error('Output not set'); + } this.output.present(result); return Result.ok(undefined); } catch (error) { diff --git a/core/racing/application/use-cases/GetDriversLeaderboardUseCase.ts b/core/racing/application/use-cases/GetDriversLeaderboardUseCase.ts index 51880bb01..087e98008 100644 --- a/core/racing/application/use-cases/GetDriversLeaderboardUseCase.ts +++ b/core/racing/application/use-cases/GetDriversLeaderboardUseCase.ts @@ -1,4 +1,4 @@ -import type { Logger } from '@core/shared/application'; +import type { Logger, UseCaseOutputPort } from '@core/shared/application'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { Driver } from '../../domain/entities/Driver'; @@ -49,13 +49,14 @@ export class GetDriversLeaderboardUseCase { private readonly driverStatsService: IDriverStatsService, private readonly getDriverAvatar: (driverId: string) => Promise, private readonly logger: Logger, + private readonly output: UseCaseOutputPort, ) {} async execute( input: GetDriversLeaderboardInput, ): Promise< Result< - GetDriversLeaderboardResult, + void, ApplicationErrorCode > > { @@ -107,7 +108,9 @@ export class GetDriversLeaderboardUseCase { this.logger.debug('Successfully computed drivers leaderboard'); - return Result.ok(result); + this.output.present(result); + + return Result.ok(undefined); } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); diff --git a/core/racing/application/use-cases/UpdateDriverProfileUseCase.ts b/core/racing/application/use-cases/UpdateDriverProfileUseCase.ts index 4e6be252f..78123a98b 100644 --- a/core/racing/application/use-cases/UpdateDriverProfileUseCase.ts +++ b/core/racing/application/use-cases/UpdateDriverProfileUseCase.ts @@ -11,9 +11,7 @@ export type UpdateDriverProfileInput = { country?: string; }; -export type UpdateDriverProfileResult = { - driverId: string; -}; +export type UpdateDriverProfileResult = Driver; export type UpdateDriverProfileErrorCode = | 'DRIVER_NOT_FOUND' @@ -64,11 +62,7 @@ export class UpdateDriverProfileUseCase implements UseCase