This commit is contained in:
2025-12-21 22:35:38 +01:00
parent 3c64f328e2
commit 9bd2e630e6
38 changed files with 736 additions and 684 deletions

View File

@@ -12,24 +12,14 @@ describe('AnalyticsController', () => {
let controller: AnalyticsController; let controller: AnalyticsController;
let service: ReturnType<typeof vi.mocked<AnalyticsService>>; let service: ReturnType<typeof vi.mocked<AnalyticsService>>;
beforeEach(async () => { beforeEach(() => {
const module: TestingModule = await Test.createTestingModule({ service = {
controllers: [AnalyticsController], recordPageView: vi.fn(),
providers: [ recordEngagement: vi.fn(),
{ getDashboardData: vi.fn(),
provide: AnalyticsService, getAnalyticsMetrics: vi.fn(),
useValue: { } as any;
recordPageView: vi.fn(), controller = new AnalyticsController(service);
recordEngagement: vi.fn(),
getDashboardData: vi.fn(),
getAnalyticsMetrics: vi.fn(),
},
},
],
}).compile();
controller = module.get<AnalyticsController>(AnalyticsController);
service = vi.mocked(module.get(AnalyticsService));
}); });
describe('recordPageView', () => { describe('recordPageView', () => {

View File

@@ -2,6 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing';
import { AnalyticsModule } from './AnalyticsModule'; import { AnalyticsModule } from './AnalyticsModule';
import { AnalyticsController } from './AnalyticsController'; import { AnalyticsController } from './AnalyticsController';
import { AnalyticsService } from './AnalyticsService'; import { AnalyticsService } from './AnalyticsService';
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
describe('AnalyticsModule', () => { describe('AnalyticsModule', () => {
let module: TestingModule; let module: TestingModule;
@@ -9,7 +10,10 @@ describe('AnalyticsModule', () => {
beforeEach(async () => { beforeEach(async () => {
module = await Test.createTestingModule({ module = await Test.createTestingModule({
imports: [AnalyticsModule], imports: [AnalyticsModule],
}).compile(); })
.overrideProvider('Logger_TOKEN')
.useClass(ConsoleLogger)
.compile();
}); });
it('should compile the module', () => { it('should compile the module', () => {

View File

@@ -1,7 +1,8 @@
import { vi } from 'vitest'; import { Mock, vi } from 'vitest';
import { AuthController } from './AuthController'; import { AuthController } from './AuthController';
import { AuthService } from './AuthService'; 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', () => { describe('AuthController', () => {
let controller: AuthController; let controller: AuthController;
@@ -36,7 +37,7 @@ describe('AuthController', () => {
displayName: 'Test User', displayName: 'Test User',
}, },
}; };
(service.signupWithEmail as jest.Mock).mockResolvedValue(session); (service.signupWithEmail as Mock).mockResolvedValue(session);
const result = await controller.signup(params); const result = await controller.signup(params);
@@ -59,7 +60,7 @@ describe('AuthController', () => {
displayName: 'Test User', displayName: 'Test User',
}, },
}; };
(service.loginWithEmail as jest.Mock).mockResolvedValue(session); (service.loginWithEmail as Mock).mockResolvedValue(session);
const result = await controller.login(params); const result = await controller.login(params);
@@ -78,7 +79,7 @@ describe('AuthController', () => {
displayName: 'Test User', displayName: 'Test User',
}, },
}; };
(service.getCurrentSession as jest.Mock).mockResolvedValue(session); (service.getCurrentSession as Mock).mockResolvedValue(session);
const result = await controller.getSession(); const result = await controller.getSession();
@@ -87,7 +88,7 @@ describe('AuthController', () => {
}); });
it('should return null if no session', async () => { 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(); const result = await controller.getSession();
@@ -97,8 +98,8 @@ describe('AuthController', () => {
describe('logout', () => { describe('logout', () => {
it('should call service.logout and return DTO', async () => { it('should call service.logout and return DTO', async () => {
const dto = { success: true }; const dto: CommandResultDTO = { success: true };
(service.logout as jest.Mock).mockResolvedValue(dto); (service.logout as Mock).mockResolvedValue(dto);
const result = await controller.logout(); const result = await controller.logout();

View File

@@ -1,6 +1,7 @@
import { Controller, Get, Post, Body } from '@nestjs/common'; import { Controller, Get, Post, Body } from '@nestjs/common';
import { AuthService } from './AuthService'; import { AuthService } from './AuthService';
import { LoginParams, SignupParams, AuthSessionDTO } from './dtos/AuthDto'; import { LoginParams, SignupParams, AuthSessionDTO } from './dtos/AuthDto';
import type { CommandResultDTO } from './presenters/CommandResultPresenter';
@Controller('auth') @Controller('auth')
export class AuthController { export class AuthController {
@@ -22,7 +23,7 @@ export class AuthController {
} }
@Post('logout') @Post('logout')
async logout(): Promise<{ success: boolean }> { async logout(): Promise<CommandResultDTO> {
return this.authService.logout(); return this.authService.logout();
} }
} }

View File

@@ -31,6 +31,8 @@ export const IDENTITY_SESSION_PORT_TOKEN = 'IdentitySessionPort';
export const LOGIN_USE_CASE_TOKEN = 'LoginUseCase'; export const LOGIN_USE_CASE_TOKEN = 'LoginUseCase';
export const SIGNUP_USE_CASE_TOKEN = 'SignupUseCase'; export const SIGNUP_USE_CASE_TOKEN = 'SignupUseCase';
export const LOGOUT_USE_CASE_TOKEN = 'LogoutUseCase'; 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[] = [ export const AuthProviders: Provider[] = [
{ {
@@ -73,20 +75,28 @@ export const AuthProviders: Provider[] = [
}, },
{ {
provide: LOGIN_USE_CASE_TOKEN, provide: LOGIN_USE_CASE_TOKEN,
useFactory: (authRepo: IAuthRepository, passwordHashing: IPasswordHashingService, logger: Logger) => useFactory: (authRepo: IAuthRepository, passwordHashing: IPasswordHashingService, logger: Logger, presenter: AuthSessionPresenter) =>
new LoginUseCase(authRepo, passwordHashing, logger), new LoginUseCase(authRepo, passwordHashing, logger, presenter),
inject: [AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN], inject: [AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN, AUTH_SESSION_PRESENTER_TOKEN],
}, },
{ {
provide: SIGNUP_USE_CASE_TOKEN, provide: SIGNUP_USE_CASE_TOKEN,
useFactory: (authRepo: IAuthRepository, passwordHashing: IPasswordHashingService, logger: Logger) => useFactory: (authRepo: IAuthRepository, passwordHashing: IPasswordHashingService, logger: Logger, presenter: AuthSessionPresenter) =>
new SignupUseCase(authRepo, passwordHashing, logger), new SignupUseCase(authRepo, passwordHashing, logger, presenter),
inject: [AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN], inject: [AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN, AUTH_SESSION_PRESENTER_TOKEN],
}, },
{ {
provide: LOGOUT_USE_CASE_TOKEN, provide: LOGOUT_USE_CASE_TOKEN,
useFactory: (sessionPort: IdentitySessionPort, logger: Logger) => useFactory: (sessionPort: IdentitySessionPort, logger: Logger, presenter: CommandResultPresenter) =>
new LogoutUseCase(sessionPort, logger), new LogoutUseCase(sessionPort, logger, presenter),
inject: [IDENTITY_SESSION_PORT_TOKEN, LOGGER_TOKEN], 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,
}, },
]; ];

View File

@@ -1,4 +1,4 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject } from '@nestjs/common';
// Core Use Cases // Core Use Cases
import { LoginUseCase, type LoginInput } from '@core/identity/application/use-cases/LoginUseCase'; 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 { IUserRepository } from '@core/identity/domain/repositories/IUserRepository';
import type { Logger } from '@core/shared/application'; 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 { 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 { AuthSessionPresenter } from './presenters/AuthSessionPresenter';
import type { CommandResultDTO } from './presenters/CommandResultPresenter'; import { CommandResultPresenter, type CommandResultDTO } from './presenters/CommandResultPresenter';
import { CommandResultPresenter } from './presenters/CommandResultPresenter';
@Injectable()
export class AuthService { export class AuthService {
constructor( constructor(
@Inject(LOGGER_TOKEN) private logger: Logger, @Inject(LOGGER_TOKEN) private logger: Logger,
@@ -25,31 +23,10 @@ export class AuthService {
@Inject(LOGIN_USE_CASE_TOKEN) private readonly loginUseCase: LoginUseCase, @Inject(LOGIN_USE_CASE_TOKEN) private readonly loginUseCase: LoginUseCase,
@Inject(SIGNUP_USE_CASE_TOKEN) private readonly signupUseCase: SignupUseCase, @Inject(SIGNUP_USE_CASE_TOKEN) private readonly signupUseCase: SignupUseCase,
@Inject(LOGOUT_USE_CASE_TOKEN) private readonly logoutUseCase: LogoutUseCase, @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<AuthSessionDTO | null> { async getCurrentSession(): Promise<AuthSessionDTO | null> {
// TODO this must call a use case
this.logger.debug('[AuthService] Attempting to get current session.'); this.logger.debug('[AuthService] Attempting to get current session.');
const coreSession = await this.identitySessionPort.getCurrentSession(); const coreSession = await this.identitySessionPort.getCurrentSession();
if (!coreSession) { if (!coreSession) {
@@ -87,7 +64,8 @@ export class AuthService {
throw new Error(error.details?.message ?? 'Signup failed'); throw new Error(error.details?.message ?? 'Signup failed');
} }
const userDTO = this.authSessionPresenter.getResponseModel(); const authSessionPresenter = new AuthSessionPresenter();
const userDTO = authSessionPresenter.getResponseModel();
const coreUserDTO = { const coreUserDTO = {
id: userDTO.userId, id: userDTO.userId,
displayName: userDTO.displayName, displayName: userDTO.displayName,
@@ -116,7 +94,8 @@ export class AuthService {
throw new Error(error.details?.message ?? 'Login failed'); throw new Error(error.details?.message ?? 'Login failed');
} }
const userDTO = this.authSessionPresenter.getResponseModel(); const authSessionPresenter = new AuthSessionPresenter();
const userDTO = authSessionPresenter.getResponseModel();
const coreUserDTO = { const coreUserDTO = {
id: userDTO.userId, id: userDTO.userId,
displayName: userDTO.displayName, displayName: userDTO.displayName,
@@ -133,6 +112,7 @@ export class AuthService {
async logout(): Promise<CommandResultDTO> { async logout(): Promise<CommandResultDTO> {
this.logger.debug('[AuthService] Attempting logout.'); this.logger.debug('[AuthService] Attempting logout.');
const commandResultPresenter = new CommandResultPresenter();
const result = await this.logoutUseCase.execute(); const result = await this.logoutUseCase.execute();
if (result.isErr()) { if (result.isErr()) {
@@ -140,6 +120,6 @@ export class AuthService {
throw new Error(error.details?.message ?? 'Logout failed'); throw new Error(error.details?.message ?? 'Logout failed');
} }
return this.commandResultPresenter.getResponseModel(); return commandResultPresenter.getResponseModel();
} }
} }

View File

@@ -5,16 +5,12 @@ import { UserId } from '@core/identity/domain/value-objects/UserId';
describe('AuthSessionPresenter', () => { describe('AuthSessionPresenter', () => {
let presenter: AuthSessionPresenter; let presenter: AuthSessionPresenter;
let mockIdentitySessionPort: any;
beforeEach(() => { beforeEach(() => {
mockIdentitySessionPort = { presenter = new AuthSessionPresenter();
createSession: vi.fn(),
};
presenter = new AuthSessionPresenter(mockIdentitySessionPort);
}); });
it('maps successful result into response model', async () => { it('maps successful result into response model', () => {
const user = User.create({ const user = User.create({
id: UserId.fromString('user-1'), id: UserId.fromString('user-1'),
displayName: 'Test User', displayName: 'Test User',
@@ -22,20 +18,15 @@ describe('AuthSessionPresenter', () => {
passwordHash: { value: 'hash' } as any, passwordHash: { value: 'hash' } as any,
}); });
const expectedSession = { const expectedUser = {
token: 'token-123', userId: 'user-1',
user: { email: 'user@example.com',
userId: 'user-1', displayName: 'Test User',
email: 'user@example.com',
displayName: 'Test User',
},
}; };
mockIdentitySessionPort.createSession.mockResolvedValue(expectedSession); presenter.present({ user });
await presenter.present({ user }); expect(presenter.getResponseModel()).toEqual(expectedUser);
expect(presenter.getResponseModel()).toEqual(expectedSession);
}); });
it('getResponseModel throws when not presented', () => { it('getResponseModel throws when not presented', () => {

View File

@@ -2,6 +2,8 @@ import { Test, TestingModule } from '@nestjs/testing';
import { DashboardModule } from './DashboardModule'; import { DashboardModule } from './DashboardModule';
import { DashboardController } from './DashboardController'; import { DashboardController } from './DashboardController';
import { DashboardService } from './DashboardService'; import { DashboardService } from './DashboardService';
import { DashboardOverviewPresenter } from './presenters/DashboardOverviewPresenter';
import { DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN } from './DashboardProviders';
describe('DashboardModule', () => { describe('DashboardModule', () => {
let module: TestingModule; let module: TestingModule;
@@ -27,4 +29,10 @@ describe('DashboardModule', () => {
expect(service).toBeDefined(); expect(service).toBeDefined();
expect(service).toBeInstanceOf(DashboardService); expect(service).toBeInstanceOf(DashboardService);
}); });
it('should bind DashboardOverviewPresenter as the output port for the use case', () => {
const presenter = module.get<DashboardOverviewPresenter>(DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN);
expect(presenter).toBeDefined();
expect(presenter).toBeInstanceOf(DashboardOverviewPresenter);
});
}); });

View File

@@ -7,14 +7,14 @@ import { DashboardOverviewPresenter } from './presenters/DashboardOverviewPresen
import type { Logger } from '@core/shared/application/Logger'; import type { Logger } from '@core/shared/application/Logger';
// Tokens // 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() @Injectable()
export class DashboardService { export class DashboardService {
constructor( constructor(
@Inject(LOGGER_TOKEN) private readonly logger: Logger, @Inject(LOGGER_TOKEN) private readonly logger: Logger,
@Inject(DASHBOARD_OVERVIEW_USE_CASE_TOKEN) private readonly dashboardOverviewUseCase: DashboardOverviewUseCase, @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<DashboardOverviewDTO> { async getDashboardOverview(driverId: string): Promise<DashboardOverviewDTO> {

View File

@@ -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 { Request } from 'express';
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger'; import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
@@ -25,16 +25,14 @@ export class DriverController {
@ApiOperation({ summary: 'Get drivers leaderboard' }) @ApiOperation({ summary: 'Get drivers leaderboard' })
@ApiResponse({ status: 200, description: 'List of drivers for the leaderboard', type: DriversLeaderboardDTO }) @ApiResponse({ status: 200, description: 'List of drivers for the leaderboard', type: DriversLeaderboardDTO })
async getDriversLeaderboard(): Promise<DriversLeaderboardDTO> { async getDriversLeaderboard(): Promise<DriversLeaderboardDTO> {
const presenter = await this.driverService.getDriversLeaderboard(); return await this.driverService.getDriversLeaderboard();
return presenter.viewModel;
} }
@Get('total-drivers') @Get('total-drivers')
@ApiOperation({ summary: 'Get the total number of drivers' }) @ApiOperation({ summary: 'Get the total number of drivers' })
@ApiResponse({ status: 200, description: 'Total number of drivers', type: DriverStatsDTO }) @ApiResponse({ status: 200, description: 'Total number of drivers', type: DriverStatsDTO })
async getTotalDrivers(): Promise<DriverStatsDTO> { async getTotalDrivers(): Promise<DriverStatsDTO> {
const presenter = await this.driverService.getTotalDrivers(); return await this.driverService.getTotalDrivers();
return presenter.viewModel;
} }
@Get('current') @Get('current')
@@ -47,8 +45,7 @@ export class DriverController {
return null; return null;
} }
const presenter = await this.driverService.getCurrentDriver(userId); return await this.driverService.getCurrentDriver(userId);
return presenter.viewModel;
} }
@Post('complete-onboarding') @Post('complete-onboarding')
@@ -59,8 +56,7 @@ export class DriverController {
@Req() req: AuthenticatedRequest, @Req() req: AuthenticatedRequest,
): Promise<CompleteOnboardingOutputDTO> { ): Promise<CompleteOnboardingOutputDTO> {
const userId = req.user!.userId; const userId = req.user!.userId;
const presenter = await this.driverService.completeOnboarding(userId, input); return await this.driverService.completeOnboarding(userId, input);
return presenter.viewModel;
} }
@Get(':driverId/races/:raceId/registration-status') @Get(':driverId/races/:raceId/registration-status')
@@ -70,8 +66,7 @@ export class DriverController {
@Param('driverId') driverId: string, @Param('driverId') driverId: string,
@Param('raceId') raceId: string, @Param('raceId') raceId: string,
): Promise<DriverRegistrationStatusDTO> { ): Promise<DriverRegistrationStatusDTO> {
const presenter = await this.driverService.getDriverRegistrationStatus({ driverId, raceId }); return await this.driverService.getDriverRegistrationStatus({ driverId, raceId });
return presenter.viewModel;
} }
@Get(':driverId') @Get(':driverId')
@@ -79,8 +74,7 @@ export class DriverController {
@ApiResponse({ status: 200, description: 'Driver data', type: GetDriverOutputDTO }) @ApiResponse({ status: 200, description: 'Driver data', type: GetDriverOutputDTO })
@ApiResponse({ status: 404, description: 'Driver not found' }) @ApiResponse({ status: 404, description: 'Driver not found' })
async getDriver(@Param('driverId') driverId: string): Promise<GetDriverOutputDTO | null> { async getDriver(@Param('driverId') driverId: string): Promise<GetDriverOutputDTO | null> {
const presenter = await this.driverService.getDriver(driverId); return await this.driverService.getDriver(driverId);
return presenter.viewModel;
} }
@Get(':driverId/profile') @Get(':driverId/profile')
@@ -88,8 +82,7 @@ export class DriverController {
@ApiResponse({ status: 200, description: 'Driver profile data', type: GetDriverProfileOutputDTO }) @ApiResponse({ status: 200, description: 'Driver profile data', type: GetDriverProfileOutputDTO })
@ApiResponse({ status: 404, description: 'Driver not found' }) @ApiResponse({ status: 404, description: 'Driver not found' })
async getDriverProfile(@Param('driverId') driverId: string): Promise<GetDriverProfileOutputDTO> { async getDriverProfile(@Param('driverId') driverId: string): Promise<GetDriverProfileOutputDTO> {
const presenter = await this.driverService.getDriverProfile(driverId); return await this.driverService.getDriverProfile(driverId);
return presenter.viewModel;
} }
@Put(':driverId/profile') @Put(':driverId/profile')
@@ -99,8 +92,7 @@ export class DriverController {
@Param('driverId') driverId: string, @Param('driverId') driverId: string,
@Body() body: { bio?: string; country?: string }, @Body() body: { bio?: string; country?: string },
): Promise<GetDriverOutputDTO | null> { ): Promise<GetDriverOutputDTO | null> {
const presenter = await this.driverService.updateDriverProfile(driverId, body.bio, body.country); return await this.driverService.updateDriverProfile(driverId, body.bio, body.country);
return presenter.viewModel;
} }
// Add other Driver endpoints here based on other presenters // Add other Driver endpoints here based on other presenters

View File

@@ -69,6 +69,13 @@ export const GET_PROFILE_OVERVIEW_USE_CASE_TOKEN = 'GetProfileOverviewUseCase';
export const DriverProviders: Provider[] = [ export const DriverProviders: Provider[] = [
DriverService, // Provide the service itself DriverService, // Provide the service itself
// Presenters
DriversLeaderboardPresenter,
DriverStatsPresenter,
CompleteOnboardingPresenter,
DriverRegistrationStatusPresenter,
DriverPresenter,
DriverProfilePresenter,
{ {
provide: DRIVER_REPOSITORY_TOKEN, provide: DRIVER_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryDriverRepository(logger), // Factory for InMemoryDriverRepository useFactory: (logger: Logger) => new InMemoryDriverRepository(logger), // Factory for InMemoryDriverRepository
@@ -138,8 +145,9 @@ export const DriverProviders: Provider[] = [
driverStatsService: IDriverStatsService, driverStatsService: IDriverStatsService,
imageService: IImageServicePort, imageService: IImageServicePort,
logger: Logger, logger: Logger,
) => new GetDriversLeaderboardUseCase(driverRepo, rankingService, driverStatsService, imageService, logger), presenter: DriversLeaderboardPresenter,
inject: [DRIVER_REPOSITORY_TOKEN, RANKING_SERVICE_TOKEN, DRIVER_STATS_SERVICE_TOKEN, IMAGE_SERVICE_PORT_TOKEN, LOGGER_TOKEN], ) => 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, provide: GET_TOTAL_DRIVERS_USE_CASE_TOKEN,
@@ -148,19 +156,19 @@ export const DriverProviders: Provider[] = [
}, },
{ {
provide: COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN, provide: COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN,
useFactory: (driverRepo: IDriverRepository) => new CompleteDriverOnboardingUseCase(driverRepo), useFactory: (driverRepo: IDriverRepository, logger: Logger, presenter: CompleteOnboardingPresenter) => new CompleteDriverOnboardingUseCase(driverRepo, logger, presenter),
inject: [DRIVER_REPOSITORY_TOKEN], inject: [DRIVER_REPOSITORY_TOKEN, LOGGER_TOKEN, CompleteOnboardingPresenter.name],
}, },
{ {
provide: IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN, provide: IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN,
useFactory: (registrationRepo: IRaceRegistrationRepository, logger: Logger) => useFactory: (registrationRepo: IRaceRegistrationRepository, logger: Logger, presenter: DriverRegistrationStatusPresenter) =>
new IsDriverRegisteredForRaceUseCase(registrationRepo, logger), new IsDriverRegisteredForRaceUseCase(registrationRepo, logger, presenter),
inject: [RACE_REGISTRATION_REPOSITORY_TOKEN, LOGGER_TOKEN], inject: [RACE_REGISTRATION_REPOSITORY_TOKEN, LOGGER_TOKEN, DriverRegistrationStatusPresenter.name],
}, },
{ {
provide: UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN, provide: UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN,
useFactory: (driverRepo: IDriverRepository) => new UpdateDriverProfileUseCase(driverRepo), useFactory: (driverRepo: IDriverRepository, presenter: DriverPresenter, logger: Logger) => new UpdateDriverProfileUseCase(driverRepo, presenter, logger),
inject: [DRIVER_REPOSITORY_TOKEN], inject: [DRIVER_REPOSITORY_TOKEN, DriverPresenter.name, LOGGER_TOKEN],
}, },
{ {
provide: GET_PROFILE_OVERVIEW_USE_CASE_TOKEN, provide: GET_PROFILE_OVERVIEW_USE_CASE_TOKEN,
@@ -173,6 +181,7 @@ export const DriverProviders: Provider[] = [
driverExtendedProfileProvider: DriverExtendedProfileProvider, driverExtendedProfileProvider: DriverExtendedProfileProvider,
driverStatsService: IDriverStatsService, driverStatsService: IDriverStatsService,
rankingService: IRankingService, rankingService: IRankingService,
presenter: DriverProfilePresenter,
) => ) =>
new GetProfileOverviewUseCase( new GetProfileOverviewUseCase(
driverRepo, driverRepo,
@@ -207,6 +216,7 @@ export const DriverProviders: Provider[] = [
rating: ranking.rating, rating: ranking.rating,
overallRank: ranking.overallRank, overallRank: ranking.overallRank,
})), })),
presenter,
), ),
inject: [ inject: [
DRIVER_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN,
@@ -217,6 +227,7 @@ export const DriverProviders: Provider[] = [
DRIVER_EXTENDED_PROFILE_PROVIDER_TOKEN, DRIVER_EXTENDED_PROFILE_PROVIDER_TOKEN,
DRIVER_STATS_SERVICE_TOKEN, DRIVER_STATS_SERVICE_TOKEN,
RANKING_SERVICE_TOKEN, RANKING_SERVICE_TOKEN,
DriverProfilePresenter.name,
], ],
}, },
]; ];

View File

@@ -14,7 +14,7 @@ import { GetTotalDriversUseCase } from '@core/racing/application/use-cases/GetTo
import { CompleteDriverOnboardingUseCase } from '@core/racing/application/use-cases/CompleteDriverOnboardingUseCase'; import { CompleteDriverOnboardingUseCase } from '@core/racing/application/use-cases/CompleteDriverOnboardingUseCase';
import { IsDriverRegisteredForRaceUseCase } from '@core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase'; import { IsDriverRegisteredForRaceUseCase } from '@core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase';
import { GetProfileOverviewUseCase } from '@core/racing/application/use-cases/GetProfileOverviewUseCase'; 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 // Presenters
import { DriverStatsPresenter } from './presenters/DriverStatsPresenter'; import { DriverStatsPresenter } from './presenters/DriverStatsPresenter';
@@ -70,10 +70,12 @@ export class DriverService {
const result = await this.getDriversLeaderboardUseCase.execute({}); const result = await this.getDriversLeaderboardUseCase.execute({});
const presenter = new DriversLeaderboardPresenter(); if (result.isErr()) {
presenter.present(result); 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<DriverStatsDTO> { async getTotalDrivers(): Promise<DriverStatsDTO> {
@@ -101,14 +103,15 @@ export class DriverService {
lastName: input.lastName, lastName: input.lastName,
displayName: input.displayName, displayName: input.displayName,
country: input.country, country: input.country,
timezone: input.timezone, ...(input.bio !== undefined ? { bio: input.bio } : {}),
bio: input.bio,
}); });
const presenter = new CompleteOnboardingPresenter(); if (result.isErr()) {
presenter.present(result); const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to complete onboarding');
}
return presenter.responseModel; return this.completeOnboardingPresenter.getResponseModel();
} }
async getDriverRegistrationStatus( async getDriverRegistrationStatus(
@@ -121,10 +124,12 @@ export class DriverService {
driverId: query.driverId, driverId: query.driverId,
}); });
const presenter = new DriverRegistrationStatusPresenter(); if (result.isErr()) {
presenter.present(result); 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<GetDriverOutputDTO | null> { async getCurrentDriver(userId: string): Promise<GetDriverOutputDTO | null> {
@@ -132,10 +137,9 @@ export class DriverService {
const driver = await this.driverRepository.findById(userId); const driver = await this.driverRepository.findById(userId);
const presenter = new DriverPresenter(); this.driverPresenter.present(driver ?? null);
presenter.present(driver ?? null);
return presenter.responseModel; return this.driverPresenter.getResponseModel();
} }
async updateDriverProfile( async updateDriverProfile(
@@ -145,19 +149,21 @@ export class DriverService {
): Promise<GetDriverOutputDTO | null> { ): Promise<GetDriverOutputDTO | null> {
this.logger.debug(`[DriverService] Updating driver profile for driverId: ${driverId}`); 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()) { if (result.isErr()) {
this.logger.error(`Failed to update driver profile: ${result.error.code}`); this.logger.error(`Failed to update driver profile: ${result.unwrapErr().code}`);
presenter.present(null); this.driverPresenter.present(null);
return presenter.responseModel; return this.driverPresenter.getResponseModel();
} }
const updatedDriver = await this.driverRepository.findById(driverId); const updatedDriver = await this.driverRepository.findById(driverId);
presenter.present(updatedDriver ?? null); this.driverPresenter.present(updatedDriver ?? null);
return presenter.responseModel; return this.driverPresenter.getResponseModel();
} }
async getDriver(driverId: string): Promise<GetDriverOutputDTO | null> { async getDriver(driverId: string): Promise<GetDriverOutputDTO | null> {
@@ -165,10 +171,9 @@ export class DriverService {
const driver = await this.driverRepository.findById(driverId); const driver = await this.driverRepository.findById(driverId);
const presenter = new DriverPresenter(); this.driverPresenter.present(driver ?? null);
presenter.present(driver ?? null);
return presenter.responseModel; return this.driverPresenter.getResponseModel();
} }
async getDriverProfile(driverId: string): Promise<GetDriverProfileOutputDTO> { async getDriverProfile(driverId: string): Promise<GetDriverProfileOutputDTO> {
@@ -176,9 +181,11 @@ export class DriverService {
const result = await this.getProfileOverviewUseCase.execute({ driverId }); const result = await this.getProfileOverviewUseCase.execute({ driverId });
const presenter = new DriverProfilePresenter(); if (result.isErr()) {
presenter.present(result); const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to load driver profile');
}
return presenter.responseModel; return this.driverProfilePresenter.getResponseModel();
} }
} }

View File

@@ -9,10 +9,10 @@ describe('DriverRegistrationStatusPresenter', () => {
}); });
describe('present', () => { describe('present', () => {
it('should map parameters to view model for registered driver', () => { it('should map parameters to response model for registered driver', () => {
presenter.present(true, 'race-123', 'driver-456'); presenter.present({ isRegistered: true, raceId: 'race-123', driverId: 'driver-456' });
const result = presenter.viewModel; const result = presenter.getResponseModel();
expect(result).toEqual({ expect(result).toEqual({
isRegistered: true, isRegistered: true,
@@ -21,10 +21,10 @@ describe('DriverRegistrationStatusPresenter', () => {
}); });
}); });
it('should map parameters to view model for unregistered driver', () => { it('should map parameters to response model for unregistered driver', () => {
presenter.present(false, 'race-789', 'driver-101'); presenter.present({ isRegistered: false, raceId: 'race-789', driverId: 'driver-101' });
const result = presenter.viewModel; const result = presenter.getResponseModel();
expect(result).toEqual({ expect(result).toEqual({
isRegistered: false, isRegistered: false,
@@ -36,11 +36,11 @@ describe('DriverRegistrationStatusPresenter', () => {
describe('reset', () => { describe('reset', () => {
it('should reset the result', () => { it('should reset the result', () => {
presenter.present(true, 'race-123', 'driver-456'); presenter.present({ isRegistered: true, raceId: 'race-123', driverId: 'driver-456' });
expect(presenter.viewModel).toBeDefined(); expect(presenter.getResponseModel()).toBeDefined();
presenter.reset(); presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented'); expect(() => presenter.getResponseModel()).toThrow('Presenter not presented');
}); });
}); });
}); });

View File

@@ -9,6 +9,10 @@ export class DriverRegistrationStatusPresenter
{ {
private responseModel: DriverRegistrationStatusDTO | null = null; private responseModel: DriverRegistrationStatusDTO | null = null;
reset(): void {
this.responseModel = null;
}
present(result: IsDriverRegisteredForRaceResult): void { present(result: IsDriverRegisteredForRaceResult): void {
this.responseModel = { this.responseModel = {
isRegistered: result.isRegistered, isRegistered: result.isRegistered,

View File

@@ -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 { DriversLeaderboardDTO } from '../dtos/DriversLeaderboardDTO';
import type { import type {
GetDriversLeaderboardResult, GetDriversLeaderboardResult,
GetDriversLeaderboardErrorCode,
} from '@core/racing/application/use-cases/GetDriversLeaderboardUseCase'; } from '@core/racing/application/use-cases/GetDriversLeaderboardUseCase';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
export type DriversLeaderboardApplicationError = ApplicationErrorCode< export class DriversLeaderboardPresenter implements UseCaseOutputPort<GetDriversLeaderboardResult> {
GetDriversLeaderboardErrorCode, private responseModel: DriversLeaderboardDTO | null = null;
{ message: string }
>;
export class DriversLeaderboardPresenter { present(result: GetDriversLeaderboardResult): void {
present( this.responseModel = {
result: Result<GetDriversLeaderboardResult, DriversLeaderboardApplicationError>, drivers: result.items.map(item => ({
): 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 => ({
id: item.driver.id, id: item.driver.id,
name: item.driver.name.toString(), name: item.driver.name.toString(),
rating: item.rating, rating: item.rating,
@@ -36,9 +22,14 @@ export class DriversLeaderboardPresenter {
rank: item.rank, rank: item.rank,
avatarUrl: item.avatarUrl, avatarUrl: item.avatarUrl,
})), })),
totalRaces: output.items.reduce((sum, d) => sum + d.racesCompleted, 0), totalRaces: result.totalRaces,
totalWins: output.items.reduce((sum, d) => sum + d.wins, 0), totalWins: result.totalWins,
activeCount: output.items.filter(d => d.isActive).length, activeCount: result.activeCount,
}; };
} }
getResponseModel(): DriversLeaderboardDTO {
if (!this.responseModel) throw new Error('Presenter not presented');
return this.responseModel;
}
} }

View File

@@ -8,7 +8,7 @@ import { IAvatarRepository } from '@core/media/domain/repositories/IAvatarReposi
import { FaceValidationPort } from '@core/media/application/ports/FaceValidationPort'; import { FaceValidationPort } from '@core/media/application/ports/FaceValidationPort';
import { AvatarGenerationPort } from '@core/media/application/ports/AvatarGenerationPort'; import { AvatarGenerationPort } from '@core/media/application/ports/AvatarGenerationPort';
import { MediaStoragePort } from '@core/media/application/ports/MediaStoragePort'; 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 use cases
import { RequestAvatarGenerationUseCase } from '@core/media/application/use-cases/RequestAvatarGenerationUseCase'; 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 { GetAvatarUseCase } from '@core/media/application/use-cases/GetAvatarUseCase';
import { UpdateAvatarUseCase } from '@core/media/application/use-cases/UpdateAvatarUseCase'; 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 // Define injection tokens
export const AVATAR_GENERATION_REPOSITORY_TOKEN = 'IAvatarGenerationRepository'; export const AVATAR_GENERATION_REPOSITORY_TOKEN = 'IAvatarGenerationRepository';
export const MEDIA_REPOSITORY_TOKEN = 'IMediaRepository'; 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 GET_AVATAR_USE_CASE_TOKEN = 'GetAvatarUseCase';
export const UPDATE_AVATAR_USE_CASE_TOKEN = 'UpdateAvatarUseCase'; 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 { AvatarGenerationRequest } from '@core/media/domain/entities/AvatarGenerationRequest';
import type { Media } from '@core/media/domain/entities/Media'; import type { Media } from '@core/media/domain/entities/Media';
import type { Avatar } from '@core/media/domain/entities/Avatar'; import type { Avatar } from '@core/media/domain/entities/Avatar';
@@ -110,6 +134,12 @@ class MockLogger implements Logger {
export const MediaProviders: Provider[] = [ export const MediaProviders: Provider[] = [
MediaService, // Provide the service itself MediaService, // Provide the service itself
RequestAvatarGenerationPresenter,
UploadMediaPresenter,
GetMediaPresenter,
DeleteMediaPresenter,
GetAvatarPresenter,
UpdateAvatarPresenter,
{ {
provide: AVATAR_GENERATION_REPOSITORY_TOKEN, provide: AVATAR_GENERATION_REPOSITORY_TOKEN,
useClass: MockAvatarGenerationRepository, useClass: MockAvatarGenerationRepository,
@@ -138,41 +168,66 @@ export const MediaProviders: Provider[] = [
provide: LOGGER_TOKEN, provide: LOGGER_TOKEN,
useClass: MockLogger, 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 // Use cases
{ {
provide: REQUEST_AVATAR_GENERATION_USE_CASE_TOKEN, provide: REQUEST_AVATAR_GENERATION_USE_CASE_TOKEN,
useFactory: (avatarRepo: IAvatarGenerationRepository, faceValidation: FaceValidationPort, avatarGeneration: AvatarGenerationPort, logger: Logger) => useFactory: (avatarRepo: IAvatarGenerationRepository, faceValidation: FaceValidationPort, avatarGeneration: AvatarGenerationPort, output: UseCaseOutputPort<RequestAvatarGenerationResult>, logger: Logger) =>
new RequestAvatarGenerationUseCase(avatarRepo, faceValidation, avatarGeneration, logger), new RequestAvatarGenerationUseCase(avatarRepo, faceValidation, avatarGeneration, output, logger),
inject: [AVATAR_GENERATION_REPOSITORY_TOKEN, FACE_VALIDATION_PORT_TOKEN, AVATAR_GENERATION_PORT_TOKEN, LOGGER_TOKEN], 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, provide: UPLOAD_MEDIA_USE_CASE_TOKEN,
useFactory: (mediaRepo: IMediaRepository, mediaStorage: MediaStoragePort, logger: Logger) => useFactory: (mediaRepo: IMediaRepository, mediaStorage: MediaStoragePort, output: UseCaseOutputPort<UploadMediaResult>, logger: Logger) =>
new UploadMediaUseCase(mediaRepo, mediaStorage, logger), new UploadMediaUseCase(mediaRepo, mediaStorage, output, logger),
inject: [MEDIA_REPOSITORY_TOKEN, MEDIA_STORAGE_PORT_TOKEN, LOGGER_TOKEN], inject: [MEDIA_REPOSITORY_TOKEN, MEDIA_STORAGE_PORT_TOKEN, UPLOAD_MEDIA_OUTPUT_PORT_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: GET_MEDIA_USE_CASE_TOKEN, provide: GET_MEDIA_USE_CASE_TOKEN,
useFactory: (mediaRepo: IMediaRepository, logger: Logger) => useFactory: (mediaRepo: IMediaRepository, output: UseCaseOutputPort<GetMediaResult>, logger: Logger) =>
new GetMediaUseCase(mediaRepo, logger), new GetMediaUseCase(mediaRepo, output, logger),
inject: [MEDIA_REPOSITORY_TOKEN, LOGGER_TOKEN], inject: [MEDIA_REPOSITORY_TOKEN, GET_MEDIA_OUTPUT_PORT_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: DELETE_MEDIA_USE_CASE_TOKEN, provide: DELETE_MEDIA_USE_CASE_TOKEN,
useFactory: (mediaRepo: IMediaRepository, mediaStorage: MediaStoragePort, logger: Logger) => useFactory: (mediaRepo: IMediaRepository, mediaStorage: MediaStoragePort, output: UseCaseOutputPort<DeleteMediaResult>, logger: Logger) =>
new DeleteMediaUseCase(mediaRepo, mediaStorage, logger), new DeleteMediaUseCase(mediaRepo, mediaStorage, output, logger),
inject: [MEDIA_REPOSITORY_TOKEN, MEDIA_STORAGE_PORT_TOKEN, LOGGER_TOKEN], inject: [MEDIA_REPOSITORY_TOKEN, MEDIA_STORAGE_PORT_TOKEN, DELETE_MEDIA_OUTPUT_PORT_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: GET_AVATAR_USE_CASE_TOKEN, provide: GET_AVATAR_USE_CASE_TOKEN,
useFactory: (avatarRepo: IAvatarRepository, logger: Logger) => useFactory: (avatarRepo: IAvatarRepository, output: UseCaseOutputPort<GetAvatarResult>, logger: Logger) =>
new GetAvatarUseCase(avatarRepo, logger), new GetAvatarUseCase(avatarRepo, output, logger),
inject: [AVATAR_REPOSITORY_TOKEN, LOGGER_TOKEN], inject: [AVATAR_REPOSITORY_TOKEN, GET_AVATAR_OUTPUT_PORT_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: UPDATE_AVATAR_USE_CASE_TOKEN, provide: UPDATE_AVATAR_USE_CASE_TOKEN,
useFactory: (avatarRepo: IAvatarRepository, logger: Logger) => useFactory: (avatarRepo: IAvatarRepository, output: UseCaseOutputPort<UpdateAvatarResult>, logger: Logger) =>
new UpdateAvatarUseCase(avatarRepo, logger), new UpdateAvatarUseCase(avatarRepo, output, logger),
inject: [AVATAR_REPOSITORY_TOKEN, LOGGER_TOKEN], inject: [AVATAR_REPOSITORY_TOKEN, UPDATE_AVATAR_OUTPUT_PORT_TOKEN, LOGGER_TOKEN],
}, },
]; ];

View File

@@ -59,6 +59,12 @@ export class MediaService {
private readonly updateAvatarUseCase: UpdateAvatarUseCase, private readonly updateAvatarUseCase: UpdateAvatarUseCase,
@Inject(LOGGER_TOKEN) @Inject(LOGGER_TOKEN)
private readonly logger: Logger, 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( async requestAvatarGeneration(
@@ -66,18 +72,23 @@ export class MediaService {
): Promise<RequestAvatarGenerationOutputDTO> { ): Promise<RequestAvatarGenerationOutputDTO> {
this.logger.debug('[MediaService] Requesting avatar generation.'); this.logger.debug('[MediaService] Requesting avatar generation.');
const presenter = new RequestAvatarGenerationPresenter();
presenter.reset();
const result = await this.requestAvatarGenerationUseCase.execute({ const result = await this.requestAvatarGenerationUseCase.execute({
userId: input.userId, userId: input.userId,
facePhotoData: input.facePhotoData, facePhotoData: input.facePhotoData,
suitColor: input.suitColor as RacingSuitColor, 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( async uploadMedia(
@@ -85,69 +96,87 @@ export class MediaService {
): Promise<UploadMediaOutputDTO> { ): Promise<UploadMediaOutputDTO> {
this.logger.debug('[MediaService] Uploading media.'); this.logger.debug('[MediaService] Uploading media.');
const presenter = new UploadMediaPresenter();
presenter.reset();
const result = await this.uploadMediaUseCase.execute({ const result = await this.uploadMediaUseCase.execute({
file: input.file, file: input.file,
uploadedBy: input.userId ?? '', 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<GetMediaOutputDTO | null> { async getMedia(mediaId: string): Promise<GetMediaOutputDTO | null> {
this.logger.debug(`[MediaService] Getting media: ${mediaId}`); this.logger.debug(`[MediaService] Getting media: ${mediaId}`);
const presenter = new GetMediaPresenter();
presenter.reset();
const result = await this.getMediaUseCase.execute({ mediaId }); 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<DeleteMediaOutputDTO> { async deleteMedia(mediaId: string): Promise<DeleteMediaOutputDTO> {
this.logger.debug(`[MediaService] Deleting media: ${mediaId}`); this.logger.debug(`[MediaService] Deleting media: ${mediaId}`);
const presenter = new DeleteMediaPresenter();
presenter.reset();
const result = await this.deleteMediaUseCase.execute({ mediaId }); 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<GetAvatarOutputDTO | null> { async getAvatar(driverId: string): Promise<GetAvatarOutputDTO | null> {
this.logger.debug(`[MediaService] Getting avatar for driver: ${driverId}`); this.logger.debug(`[MediaService] Getting avatar for driver: ${driverId}`);
const presenter = new GetAvatarPresenter();
presenter.reset();
const result = await this.getAvatarUseCase.execute({ driverId }); 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<UpdateAvatarOutputDTO> { async updateAvatar(driverId: string, input: UpdateAvatarInput): Promise<UpdateAvatarOutputDTO> {
this.logger.debug(`[MediaService] Updating avatar for driver: ${driverId}`); this.logger.debug(`[MediaService] Updating avatar for driver: ${driverId}`);
const presenter = new UpdateAvatarPresenter();
presenter.reset();
const result = await this.updateAvatarUseCase.execute({ const result = await this.updateAvatarUseCase.execute({
driverId, 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;
} }
} }

View File

@@ -1,41 +1,19 @@
import type { Result } from '@core/shared/application/Result'; import type { UseCaseOutputPort } from '@core/shared/application';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { DeleteMediaResult } from '@core/media/application/use-cases/DeleteMediaUseCase';
import type {
DeleteMediaResult,
DeleteMediaErrorCode,
} from '@core/media/application/use-cases/DeleteMediaUseCase';
import type { DeleteMediaOutputDTO } from '../dtos/DeleteMediaOutputDTO'; import type { DeleteMediaOutputDTO } from '../dtos/DeleteMediaOutputDTO';
type DeleteMediaResponseModel = DeleteMediaOutputDTO; type DeleteMediaResponseModel = DeleteMediaOutputDTO;
export type DeleteMediaApplicationError = ApplicationErrorCode< export class DeleteMediaPresenter implements UseCaseOutputPort<DeleteMediaResult> {
DeleteMediaErrorCode,
{ message: string }
>;
export class DeleteMediaPresenter {
private model: DeleteMediaResponseModel | null = null; private model: DeleteMediaResponseModel | null = null;
reset(): void { reset(): void {
this.model = null; this.model = null;
} }
present(result: Result<DeleteMediaResult, DeleteMediaApplicationError>): void { present(result: DeleteMediaResult): 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();
this.model = { this.model = {
success: output.deleted, success: result.deleted,
error: undefined,
}; };
} }

View File

@@ -1,41 +1,19 @@
import type { Result } from '@core/shared/application/Result'; import type { UseCaseOutputPort } from '@core/shared/application';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { GetAvatarResult } from '@core/media/application/use-cases/GetAvatarUseCase';
import type {
GetAvatarResult,
GetAvatarErrorCode,
} from '@core/media/application/use-cases/GetAvatarUseCase';
import type { GetAvatarOutputDTO } from '../dtos/GetAvatarOutputDTO'; import type { GetAvatarOutputDTO } from '../dtos/GetAvatarOutputDTO';
export type GetAvatarResponseModel = GetAvatarOutputDTO | null; export type GetAvatarResponseModel = GetAvatarOutputDTO | null;
export type GetAvatarApplicationError = ApplicationErrorCode< export class GetAvatarPresenter implements UseCaseOutputPort<GetAvatarResult> {
GetAvatarErrorCode,
{ message: string }
>;
export class GetAvatarPresenter {
private model: GetAvatarResponseModel | null = null; private model: GetAvatarResponseModel | null = null;
reset(): void { reset(): void {
this.model = null; this.model = null;
} }
present(result: Result<GetAvatarResult, GetAvatarApplicationError>): void { present(result: GetAvatarResult): 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();
this.model = { this.model = {
avatarUrl: output.avatar.mediaUrl, avatarUrl: result.avatar.mediaUrl,
}; };
} }

View File

@@ -1,37 +1,18 @@
import type { Result } from '@core/shared/application/Result'; import type { UseCaseOutputPort } from '@core/shared/application';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { GetMediaResult } from '@core/media/application/use-cases/GetMediaUseCase';
import type { GetMediaResult, GetMediaErrorCode } from '@core/media/application/use-cases/GetMediaUseCase';
import type { GetMediaOutputDTO } from '../dtos/GetMediaOutputDTO'; import type { GetMediaOutputDTO } from '../dtos/GetMediaOutputDTO';
export type GetMediaResponseModel = GetMediaOutputDTO | null; export type GetMediaResponseModel = GetMediaOutputDTO | null;
export type GetMediaApplicationError = ApplicationErrorCode< export class GetMediaPresenter implements UseCaseOutputPort<GetMediaResult> {
GetMediaErrorCode,
{ message: string }
>;
export class GetMediaPresenter {
private model: GetMediaResponseModel | null = null; private model: GetMediaResponseModel | null = null;
reset(): void { reset(): void {
this.model = null; this.model = null;
} }
present(result: Result<GetMediaResult, GetMediaApplicationError>): void { present(result: GetMediaResult): void {
if (result.isErr()) { const media = result.media;
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;
this.model = { this.model = {
id: media.id, id: media.id,

View File

@@ -1,50 +1,21 @@
import type { Result } from '@core/shared/application/Result'; import type { UseCaseOutputPort } from '@core/shared/application';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { RequestAvatarGenerationResult } from '@core/media/application/use-cases/RequestAvatarGenerationUseCase';
import type {
RequestAvatarGenerationResult,
RequestAvatarGenerationErrorCode,
} from '@core/media/application/use-cases/RequestAvatarGenerationUseCase';
import type { RequestAvatarGenerationOutputDTO } from '../dtos/RequestAvatarGenerationOutputDTO'; import type { RequestAvatarGenerationOutputDTO } from '../dtos/RequestAvatarGenerationOutputDTO';
type RequestAvatarGenerationResponseModel = RequestAvatarGenerationOutputDTO; type RequestAvatarGenerationResponseModel = RequestAvatarGenerationOutputDTO;
export type RequestAvatarGenerationApplicationError = ApplicationErrorCode< export class RequestAvatarGenerationPresenter implements UseCaseOutputPort<RequestAvatarGenerationResult> {
RequestAvatarGenerationErrorCode,
{ message: string }
>;
export class RequestAvatarGenerationPresenter {
private model: RequestAvatarGenerationResponseModel | null = null; private model: RequestAvatarGenerationResponseModel | null = null;
reset() { reset() {
this.model = null; this.model = null;
} }
present( present(result: RequestAvatarGenerationResult): void {
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();
this.model = { this.model = {
success: output.status === 'completed', success: result.status === 'completed',
requestId: output.requestId, requestId: result.requestId,
avatarUrls: output.avatarUrls, avatarUrls: result.avatarUrls || [],
errorMessage: undefined,
}; };
} }

View File

@@ -1,36 +1,19 @@
import type { Result } from '@core/shared/application/Result'; import type { UseCaseOutputPort } from '@core/shared/application';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { UpdateAvatarResult } from '@core/media/application/use-cases/UpdateAvatarUseCase';
import type {
UpdateAvatarResult,
UpdateAvatarErrorCode,
} from '@core/media/application/use-cases/UpdateAvatarUseCase';
import type { UpdateAvatarOutputDTO } from '../dtos/UpdateAvatarOutputDTO'; import type { UpdateAvatarOutputDTO } from '../dtos/UpdateAvatarOutputDTO';
type UpdateAvatarResponseModel = UpdateAvatarOutputDTO; type UpdateAvatarResponseModel = UpdateAvatarOutputDTO;
export type UpdateAvatarApplicationError = ApplicationErrorCode< export class UpdateAvatarPresenter implements UseCaseOutputPort<UpdateAvatarResult> {
UpdateAvatarErrorCode,
{ message: string }
>;
export class UpdateAvatarPresenter {
private model: UpdateAvatarResponseModel | null = null; private model: UpdateAvatarResponseModel | null = null;
reset(): void { reset(): void {
this.model = null; this.model = null;
} }
present(result: Result<UpdateAvatarResult, UpdateAvatarApplicationError>): void { present(result: UpdateAvatarResult): void {
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to update avatar');
}
const output = result.unwrap();
this.model = { this.model = {
success: true, success: true,
error: undefined,
}; };
} }

View File

@@ -1,43 +1,21 @@
import type { Result } from '@core/shared/application/Result'; import type { UseCaseOutputPort } from '@core/shared/application';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { UploadMediaResult } from '@core/media/application/use-cases/UploadMediaUseCase';
import type {
UploadMediaResult,
UploadMediaErrorCode,
} from '@core/media/application/use-cases/UploadMediaUseCase';
import type { UploadMediaOutputDTO } from '../dtos/UploadMediaOutputDTO'; import type { UploadMediaOutputDTO } from '../dtos/UploadMediaOutputDTO';
type UploadMediaResponseModel = UploadMediaOutputDTO; type UploadMediaResponseModel = UploadMediaOutputDTO;
export type UploadMediaApplicationError = ApplicationErrorCode< export class UploadMediaPresenter implements UseCaseOutputPort<UploadMediaResult> {
UploadMediaErrorCode,
{ message: string }
>;
export class UploadMediaPresenter {
private model: UploadMediaResponseModel | null = null; private model: UploadMediaResponseModel | null = null;
reset(): void { reset(): void {
this.model = null; this.model = null;
} }
present(result: Result<UploadMediaResult, UploadMediaApplicationError>): void { present(result: UploadMediaResult): void {
if (result.isErr()) {
const error = result.unwrapErr();
this.model = {
success: false,
error: error.details?.message ?? 'Upload failed',
};
return;
}
const output = result.unwrap();
this.model = { this.model = {
success: true, success: true,
mediaId: output.mediaId, mediaId: result.mediaId,
url: output.url, url: result.url,
error: undefined,
}; };
} }

View File

@@ -12,8 +12,7 @@ export class PaymentsController {
@ApiOperation({ summary: 'Get payments based on filters' }) @ApiOperation({ summary: 'Get payments based on filters' })
@ApiResponse({ status: 200, description: 'List of payments', type: GetPaymentsOutput }) @ApiResponse({ status: 200, description: 'List of payments', type: GetPaymentsOutput })
async getPayments(@Query() query: GetPaymentsQuery): Promise<GetPaymentsOutput> { async getPayments(@Query() query: GetPaymentsQuery): Promise<GetPaymentsOutput> {
const presenter = await this.paymentsService.getPayments(query); return this.paymentsService.getPayments(query);
return presenter.viewModel;
} }
@Post() @Post()
@@ -21,16 +20,14 @@ export class PaymentsController {
@ApiOperation({ summary: 'Create a new payment' }) @ApiOperation({ summary: 'Create a new payment' })
@ApiResponse({ status: 201, description: 'Payment created', type: CreatePaymentOutput }) @ApiResponse({ status: 201, description: 'Payment created', type: CreatePaymentOutput })
async createPayment(@Body() input: CreatePaymentInput): Promise<CreatePaymentOutput> { async createPayment(@Body() input: CreatePaymentInput): Promise<CreatePaymentOutput> {
const presenter = await this.paymentsService.createPayment(input); return this.paymentsService.createPayment(input);
return presenter.viewModel;
} }
@Patch('status') @Patch('status')
@ApiOperation({ summary: 'Update the status of a payment' }) @ApiOperation({ summary: 'Update the status of a payment' })
@ApiResponse({ status: 200, description: 'Payment status updated', type: UpdatePaymentStatusOutput }) @ApiResponse({ status: 200, description: 'Payment status updated', type: UpdatePaymentStatusOutput })
async updatePaymentStatus(@Body() input: UpdatePaymentStatusInput): Promise<UpdatePaymentStatusOutput> { async updatePaymentStatus(@Body() input: UpdatePaymentStatusInput): Promise<UpdatePaymentStatusOutput> {
const presenter = await this.paymentsService.updatePaymentStatus(input); return this.paymentsService.updatePaymentStatus(input);
return presenter.viewModel;
} }
@Get('membership-fees') @Get('membership-fees')

View File

@@ -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 { IMembershipFeeRepository, IMemberPaymentRepository } from '@core/payments/domain/repositories/IMembershipFeeRepository';
import type { IPrizeRepository } from '@core/payments/domain/repositories/IPrizeRepository'; import type { IPrizeRepository } from '@core/payments/domain/repositories/IPrizeRepository';
import type { IWalletRepository, ITransactionRepository } from '@core/payments/domain/repositories/IWalletRepository'; 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 use cases
import { GetPaymentsUseCase } from '@core/payments/application/use-cases/GetPaymentsUseCase'; 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 { InMemoryWalletRepository, InMemoryTransactionRepository } from '@adapters/payments/persistence/inmemory/InMemoryWalletRepository';
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger'; 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 // Repository injection tokens
export const PAYMENT_REPOSITORY_TOKEN = 'IPaymentRepository'; export const PAYMENT_REPOSITORY_TOKEN = 'IPaymentRepository';
export const MEMBERSHIP_FEE_REPOSITORY_TOKEN = 'IMembershipFeeRepository'; 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 GET_WALLET_USE_CASE_TOKEN = 'GetWalletUseCase';
export const PROCESS_WALLET_TRANSACTION_USE_CASE_TOKEN = 'ProcessWalletTransactionUseCase'; 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[] = [ export const PaymentsProviders: Provider[] = [
PaymentsService, 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 // Logger
{ {
provide: LOGGER_TOKEN, provide: LOGGER_TOKEN,
@@ -96,66 +188,66 @@ export const PaymentsProviders: Provider[] = [
// Use cases (use cases receive repositories, services receive use cases) // Use cases (use cases receive repositories, services receive use cases)
{ {
provide: GET_PAYMENTS_USE_CASE_TOKEN, provide: GET_PAYMENTS_USE_CASE_TOKEN,
useFactory: (paymentRepo: IPaymentRepository) => new GetPaymentsUseCase(paymentRepo), useFactory: (paymentRepo: IPaymentRepository, output: UseCaseOutputPort<any>) => new GetPaymentsUseCase(paymentRepo, output),
inject: [PAYMENT_REPOSITORY_TOKEN], inject: [PAYMENT_REPOSITORY_TOKEN, GET_PAYMENTS_OUTPUT_PORT_TOKEN],
}, },
{ {
provide: CREATE_PAYMENT_USE_CASE_TOKEN, provide: CREATE_PAYMENT_USE_CASE_TOKEN,
useFactory: (paymentRepo: IPaymentRepository) => new CreatePaymentUseCase(paymentRepo), useFactory: (paymentRepo: IPaymentRepository, output: UseCaseOutputPort<any>) => new CreatePaymentUseCase(paymentRepo, output),
inject: [PAYMENT_REPOSITORY_TOKEN], inject: [PAYMENT_REPOSITORY_TOKEN, CREATE_PAYMENT_OUTPUT_PORT_TOKEN],
}, },
{ {
provide: UPDATE_PAYMENT_STATUS_USE_CASE_TOKEN, provide: UPDATE_PAYMENT_STATUS_USE_CASE_TOKEN,
useFactory: (paymentRepo: IPaymentRepository) => new UpdatePaymentStatusUseCase(paymentRepo), useFactory: (paymentRepo: IPaymentRepository, output: UseCaseOutputPort<any>) => new UpdatePaymentStatusUseCase(paymentRepo, output),
inject: [PAYMENT_REPOSITORY_TOKEN], inject: [PAYMENT_REPOSITORY_TOKEN, UPDATE_PAYMENT_STATUS_OUTPUT_PORT_TOKEN],
}, },
{ {
provide: GET_MEMBERSHIP_FEES_USE_CASE_TOKEN, provide: GET_MEMBERSHIP_FEES_USE_CASE_TOKEN,
useFactory: (membershipFeeRepo: IMembershipFeeRepository, memberPaymentRepo: IMemberPaymentRepository) => useFactory: (membershipFeeRepo: IMembershipFeeRepository, memberPaymentRepo: IMemberPaymentRepository, output: UseCaseOutputPort<any>) =>
new GetMembershipFeesUseCase(membershipFeeRepo, memberPaymentRepo), new GetMembershipFeesUseCase(membershipFeeRepo, memberPaymentRepo, output),
inject: [MEMBERSHIP_FEE_REPOSITORY_TOKEN, MEMBER_PAYMENT_REPOSITORY_TOKEN], inject: [MEMBERSHIP_FEE_REPOSITORY_TOKEN, MEMBER_PAYMENT_REPOSITORY_TOKEN, GET_MEMBERSHIP_FEES_OUTPUT_PORT_TOKEN],
}, },
{ {
provide: UPSERT_MEMBERSHIP_FEE_USE_CASE_TOKEN, provide: UPSERT_MEMBERSHIP_FEE_USE_CASE_TOKEN,
useFactory: (membershipFeeRepo: IMembershipFeeRepository) => new UpsertMembershipFeeUseCase(membershipFeeRepo), useFactory: (membershipFeeRepo: IMembershipFeeRepository, output: UseCaseOutputPort<any>) => new UpsertMembershipFeeUseCase(membershipFeeRepo, output),
inject: [MEMBERSHIP_FEE_REPOSITORY_TOKEN], inject: [MEMBERSHIP_FEE_REPOSITORY_TOKEN, UPSERT_MEMBERSHIP_FEE_OUTPUT_PORT_TOKEN],
}, },
{ {
provide: UPDATE_MEMBER_PAYMENT_USE_CASE_TOKEN, provide: UPDATE_MEMBER_PAYMENT_USE_CASE_TOKEN,
useFactory: (membershipFeeRepo: IMembershipFeeRepository, memberPaymentRepo: IMemberPaymentRepository) => useFactory: (membershipFeeRepo: IMembershipFeeRepository, memberPaymentRepo: IMemberPaymentRepository, output: UseCaseOutputPort<any>) =>
new UpdateMemberPaymentUseCase(membershipFeeRepo, memberPaymentRepo), new UpdateMemberPaymentUseCase(membershipFeeRepo, memberPaymentRepo, output),
inject: [MEMBERSHIP_FEE_REPOSITORY_TOKEN, MEMBER_PAYMENT_REPOSITORY_TOKEN], inject: [MEMBERSHIP_FEE_REPOSITORY_TOKEN, MEMBER_PAYMENT_REPOSITORY_TOKEN, UPDATE_MEMBER_PAYMENT_OUTPUT_PORT_TOKEN],
}, },
{ {
provide: GET_PRIZES_USE_CASE_TOKEN, provide: GET_PRIZES_USE_CASE_TOKEN,
useFactory: (prizeRepo: IPrizeRepository) => new GetPrizesUseCase(prizeRepo), useFactory: (prizeRepo: IPrizeRepository, output: UseCaseOutputPort<any>) => new GetPrizesUseCase(prizeRepo, output),
inject: [PRIZE_REPOSITORY_TOKEN], inject: [PRIZE_REPOSITORY_TOKEN, GET_PRIZES_OUTPUT_PORT_TOKEN],
}, },
{ {
provide: CREATE_PRIZE_USE_CASE_TOKEN, provide: CREATE_PRIZE_USE_CASE_TOKEN,
useFactory: (prizeRepo: IPrizeRepository) => new CreatePrizeUseCase(prizeRepo), useFactory: (prizeRepo: IPrizeRepository, output: UseCaseOutputPort<any>) => new CreatePrizeUseCase(prizeRepo, output),
inject: [PRIZE_REPOSITORY_TOKEN], inject: [PRIZE_REPOSITORY_TOKEN, CREATE_PRIZE_OUTPUT_PORT_TOKEN],
}, },
{ {
provide: AWARD_PRIZE_USE_CASE_TOKEN, provide: AWARD_PRIZE_USE_CASE_TOKEN,
useFactory: (prizeRepo: IPrizeRepository) => new AwardPrizeUseCase(prizeRepo), useFactory: (prizeRepo: IPrizeRepository, output: UseCaseOutputPort<any>) => new AwardPrizeUseCase(prizeRepo, output),
inject: [PRIZE_REPOSITORY_TOKEN], inject: [PRIZE_REPOSITORY_TOKEN, AWARD_PRIZE_OUTPUT_PORT_TOKEN],
}, },
{ {
provide: DELETE_PRIZE_USE_CASE_TOKEN, provide: DELETE_PRIZE_USE_CASE_TOKEN,
useFactory: (prizeRepo: IPrizeRepository) => new DeletePrizeUseCase(prizeRepo), useFactory: (prizeRepo: IPrizeRepository, output: UseCaseOutputPort<any>) => new DeletePrizeUseCase(prizeRepo, output),
inject: [PRIZE_REPOSITORY_TOKEN], inject: [PRIZE_REPOSITORY_TOKEN, DELETE_PRIZE_OUTPUT_PORT_TOKEN],
}, },
{ {
provide: GET_WALLET_USE_CASE_TOKEN, provide: GET_WALLET_USE_CASE_TOKEN,
useFactory: (walletRepo: IWalletRepository, transactionRepo: ITransactionRepository) => useFactory: (walletRepo: IWalletRepository, transactionRepo: ITransactionRepository, output: UseCaseOutputPort<any>) =>
new GetWalletUseCase(walletRepo, transactionRepo), new GetWalletUseCase(walletRepo, transactionRepo, output),
inject: [WALLET_REPOSITORY_TOKEN, TRANSACTION_REPOSITORY_TOKEN], inject: [WALLET_REPOSITORY_TOKEN, TRANSACTION_REPOSITORY_TOKEN, GET_WALLET_OUTPUT_PORT_TOKEN],
}, },
{ {
provide: PROCESS_WALLET_TRANSACTION_USE_CASE_TOKEN, provide: PROCESS_WALLET_TRANSACTION_USE_CASE_TOKEN,
useFactory: (walletRepo: IWalletRepository, transactionRepo: ITransactionRepository) => useFactory: (walletRepo: IWalletRepository, transactionRepo: ITransactionRepository, output: UseCaseOutputPort<any>) =>
new ProcessWalletTransactionUseCase(walletRepo, transactionRepo), new ProcessWalletTransactionUseCase(walletRepo, transactionRepo, output),
inject: [WALLET_REPOSITORY_TOKEN, TRANSACTION_REPOSITORY_TOKEN], inject: [WALLET_REPOSITORY_TOKEN, TRANSACTION_REPOSITORY_TOKEN, PROCESS_WALLET_TRANSACTION_OUTPUT_PORT_TOKEN],
}, },
]; ];

View File

@@ -90,54 +90,78 @@ export class PaymentsService {
@Inject(GET_WALLET_USE_CASE_TOKEN) private readonly getWalletUseCase: GetWalletUseCase, @Inject(GET_WALLET_USE_CASE_TOKEN) private readonly getWalletUseCase: GetWalletUseCase,
@Inject(PROCESS_WALLET_TRANSACTION_USE_CASE_TOKEN) private readonly processWalletTransactionUseCase: ProcessWalletTransactionUseCase, @Inject(PROCESS_WALLET_TRANSACTION_USE_CASE_TOKEN) private readonly processWalletTransactionUseCase: ProcessWalletTransactionUseCase,
@Inject(LOGGER_TOKEN) private readonly logger: Logger, @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<GetPaymentsPresenter> { async getPayments(query: GetPaymentsQuery): Promise<GetPaymentsOutput> {
this.logger.debug('[PaymentsService] Getting payments', { query }); this.logger.debug('[PaymentsService] Getting payments', { query });
const presenter = new GetPaymentsPresenter(); const result = await this.getPaymentsUseCase.execute(query);
await this.getPaymentsUseCase.execute(query, presenter); if (result.isErr()) {
return presenter; throw new Error(result.unwrapErr().details?.message ?? 'Failed to get payments');
}
return this.getPaymentsPresenter.getResponseModel();
} }
async createPayment(input: CreatePaymentInput): Promise<CreatePaymentPresenter> { async createPayment(input: CreatePaymentInput): Promise<CreatePaymentOutput> {
this.logger.debug('[PaymentsService] Creating payment', { input }); this.logger.debug('[PaymentsService] Creating payment', { input });
const presenter = new CreatePaymentPresenter(); const result = await this.createPaymentUseCase.execute(input);
await this.createPaymentUseCase.execute(input, presenter); if (result.isErr()) {
return presenter; throw new Error(result.unwrapErr().details?.message ?? 'Failed to create payment');
}
return this.createPaymentPresenter.getResponseModel();
} }
async updatePaymentStatus(input: UpdatePaymentStatusInput): Promise<UpdatePaymentStatusPresenter> { async updatePaymentStatus(input: UpdatePaymentStatusInput): Promise<UpdatePaymentStatusOutput> {
this.logger.debug('[PaymentsService] Updating payment status', { input }); this.logger.debug('[PaymentsService] Updating payment status', { input });
const presenter = new UpdatePaymentStatusPresenter(); const result = await this.updatePaymentStatusUseCase.execute(input);
await this.updatePaymentStatusUseCase.execute(input, presenter); if (result.isErr()) {
return presenter; throw new Error(result.unwrapErr().details?.message ?? 'Failed to update payment status');
}
return this.updatePaymentStatusPresenter.getResponseModel();
} }
async getMembershipFees(query: GetMembershipFeesQuery): Promise<GetMembershipFeesPresenter> { async getMembershipFees(query: GetMembershipFeesQuery): Promise<GetMembershipFeesOutput> {
this.logger.debug('[PaymentsService] Getting membership fees', { query }); this.logger.debug('[PaymentsService] Getting membership fees', { query });
const presenter = new GetMembershipFeesPresenter(); const result = await this.getMembershipFeesUseCase.execute(query);
await this.getMembershipFeesUseCase.execute(query, presenter); if (result.isErr()) {
return presenter; throw new Error(result.unwrapErr().details?.message ?? 'Failed to get membership fees');
}
return this.getMembershipFeesPresenter.getResponseModel();
} }
async upsertMembershipFee(input: UpsertMembershipFeeInput): Promise<UpsertMembershipFeePresenter> { async upsertMembershipFee(input: UpsertMembershipFeeInput): Promise<UpsertMembershipFeeOutput> {
this.logger.debug('[PaymentsService] Upserting membership fee', { input }); this.logger.debug('[PaymentsService] Upserting membership fee', { input });
const presenter = new UpsertMembershipFeePresenter(); const result = await this.upsertMembershipFeeUseCase.execute(input);
await this.upsertMembershipFeeUseCase.execute(input, presenter); if (result.isErr()) {
return presenter; throw new Error(result.unwrapErr().details?.message ?? 'Failed to upsert membership fee');
}
return this.upsertMembershipFeePresenter.getResponseModel();
} }
async updateMemberPayment(input: UpdateMemberPaymentInput): Promise<UpdateMemberPaymentPresenter> { async updateMemberPayment(input: UpdateMemberPaymentInput): Promise<UpdateMemberPaymentOutput> {
this.logger.debug('[PaymentsService] Updating member payment', { input }); this.logger.debug('[PaymentsService] Updating member payment', { input });
const presenter = new UpdateMemberPaymentPresenter(); const result = await this.updateMemberPaymentUseCase.execute(input);
await this.updateMemberPaymentUseCase.execute(input, presenter); if (result.isErr()) {
return presenter; throw new Error(result.unwrapErr().details?.message ?? 'Failed to update member payment');
}
return this.updateMemberPaymentPresenter.getResponseModel();
} }
async getPrizes(query: GetPrizesQuery): Promise<GetPrizesPresenter> { async getPrizes(query: GetPrizesQuery): Promise<GetPrizesPresenter> {

View File

@@ -1,25 +1,34 @@
import type { import type { UseCaseOutputPort } from '@core/shared/application';
ICreatePaymentPresenter, import type { CreatePaymentResult } from '@core/payments/application/use-cases/CreatePaymentUseCase';
CreatePaymentResultDTO, import type { CreatePaymentOutput } from '../dtos/PaymentsDto';
CreatePaymentViewModel,
} from '@core/payments/application/presenters/ICreatePaymentPresenter';
export class CreatePaymentPresenter implements ICreatePaymentPresenter { export class CreatePaymentPresenter implements UseCaseOutputPort<CreatePaymentResult> {
private responseModel: CreatePaymentViewModel | null = null; private responseModel: CreatePaymentOutput | null = null;
reset() { reset() {
this.responseModel = null; this.responseModel = null;
} }
present(dto: CreatePaymentResultDTO) { present(result: CreatePaymentResult): void {
this.responseModel = dto; 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 { getResponseModel(): CreatePaymentOutput {
return this.responseModel;
}
get responseModel(): CreatePaymentViewModel {
if (!this.responseModel) throw new Error('Presenter not presented'); if (!this.responseModel) throw new Error('Presenter not presented');
return this.responseModel; return this.responseModel;
} }

View File

@@ -1,26 +1,35 @@
import type { import type { UseCaseOutputPort } from '@core/shared/application';
IGetPaymentsPresenter, import type { GetPaymentsResult } from '@core/payments/application/use-cases/GetPaymentsUseCase';
GetPaymentsResultDTO, import type { GetPaymentsOutput } from '../dtos/PaymentsDto';
GetPaymentsViewModel,
} from '@core/payments/application/presenters/IGetPaymentsPresenter';
export class GetPaymentsPresenter implements IGetPaymentsPresenter { export class GetPaymentsPresenter implements UseCaseOutputPort<GetPaymentsResult> {
private result: GetPaymentsViewModel | null = null; private responseModel: GetPaymentsOutput | null = null;
reset() { reset() {
this.result = null; this.responseModel = null;
} }
present(dto: GetPaymentsResultDTO) { present(result: GetPaymentsResult): void {
this.result = dto; 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 { getResponseModel(): GetPaymentsOutput {
return this.result; if (!this.responseModel) throw new Error('Presenter not presented');
} return this.responseModel;
get viewModel(): GetPaymentsViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
} }
} }

View File

@@ -1,26 +1,35 @@
import type { import type { UseCaseOutputPort } from '@core/shared/application';
IUpdatePaymentStatusPresenter, import type { UpdatePaymentStatusResult } from '@core/payments/application/use-cases/UpdatePaymentStatusUseCase';
UpdatePaymentStatusResultDTO, import type { UpdatePaymentStatusOutput } from '../dtos/PaymentsDto';
UpdatePaymentStatusViewModel,
} from '@core/payments/application/presenters/IUpdatePaymentStatusPresenter';
export class UpdatePaymentStatusPresenter implements IUpdatePaymentStatusPresenter { export class UpdatePaymentStatusPresenter implements UseCaseOutputPort<UpdatePaymentStatusResult> {
private result: UpdatePaymentStatusViewModel | null = null; private responseModel: UpdatePaymentStatusOutput | null = null;
reset() { reset() {
this.result = null; this.responseModel = null;
} }
present(dto: UpdatePaymentStatusResultDTO) { present(result: UpdatePaymentStatusResult): void {
this.result = dto; 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 { getResponseModel(): UpdatePaymentStatusOutput {
return this.result; if (!this.responseModel) throw new Error('Presenter not presented');
} return this.responseModel;
get viewModel(): UpdatePaymentStatusViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
} }
} }

View File

@@ -1,8 +1,5 @@
import type { Result } from '@core/shared/application/Result'; import type { UseCaseOutputPort } from '@core/shared/application';
import type { import type { ReviewProtestResult } from '@core/racing/application/use-cases/ReviewProtestUseCase';
ReviewProtestResult,
ReviewProtestApplicationError,
} from '@core/racing/application/use-cases/ReviewProtestUseCase';
export interface ReviewProtestResponseDTO { export interface ReviewProtestResponseDTO {
success: boolean; success: boolean;
@@ -13,34 +10,18 @@ export interface ReviewProtestResponseDTO {
decision?: 'uphold' | 'dismiss'; decision?: 'uphold' | 'dismiss';
} }
export class ReviewProtestPresenter { export class ReviewProtestPresenter implements UseCaseOutputPort<ReviewProtestResult> {
private model: ReviewProtestResponseDTO | null = null; private model: ReviewProtestResponseDTO | null = null;
reset(): void { reset(): void {
this.model = null; this.model = null;
} }
present( present(result: ReviewProtestResult): void {
result: Result<ReviewProtestResult, ReviewProtestApplicationError>,
): void {
if (result.isErr()) {
const error = result.unwrapErr();
this.model = {
success: false,
errorCode: error.code,
message: error.details?.message,
};
return;
}
const value = result.unwrap();
this.model = { this.model = {
success: true, success: true,
protestId: value.protestId, protestId: result.protestId,
stewardId: value.stewardId, decision: result.status === 'upheld' ? 'uphold' : 'dismiss',
decision: value.decision,
}; };
} }

View File

@@ -96,11 +96,14 @@ export class RaceService {
async getAllRaces(): Promise<GetAllRacesPresenter> { async getAllRaces(): Promise<GetAllRacesPresenter> {
this.logger.debug('[RaceService] Fetching all races.'); this.logger.debug('[RaceService] Fetching all races.');
const presenter = new GetAllRacesPresenter();
this.getAllRacesUseCase.setOutput(presenter);
const result = await this.getAllRacesUseCase.execute({}); const result = await this.getAllRacesUseCase.execute({});
const presenter = new GetAllRacesPresenter(); if (result.isErr()) {
presenter.reset(); throw new Error(result.unwrapErr().code);
presenter.present(result); }
return presenter; return presenter;
} }

View File

@@ -1,37 +1,17 @@
import type { Result } from '@core/shared/application/Result'; import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { GetAllRacesResult } from '@core/racing/application/use-cases/GetAllRacesUseCase';
import type {
GetAllRacesResult,
GetAllRacesErrorCode,
} from '@core/racing/application/use-cases/GetAllRacesUseCase';
import type { AllRacesPageDTO } from '../dtos/AllRacesPageDTO'; import type { AllRacesPageDTO } from '../dtos/AllRacesPageDTO';
export type GetAllRacesResponseModel = AllRacesPageDTO; export type GetAllRacesResponseModel = AllRacesPageDTO;
export type GetAllRacesApplicationError = ApplicationErrorCode< export class GetAllRacesPresenter implements UseCaseOutputPort<GetAllRacesResult> {
GetAllRacesErrorCode,
{ message: string }
>;
export class GetAllRacesPresenter {
private model: GetAllRacesResponseModel | null = null; private model: GetAllRacesResponseModel | null = null;
reset(): void { present(result: GetAllRacesResult): void {
this.model = null;
}
present(result: Result<GetAllRacesResult, GetAllRacesApplicationError>): void {
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to get all races');
}
const output = result.unwrap();
const leagueMap = new Map<string, string>(); const leagueMap = new Map<string, string>();
const uniqueLeagues = new Map<string, { id: string; name: string }>(); const uniqueLeagues = new Map<string, { id: string; name: string }>();
for (const league of output.leagues) { for (const league of result.leagues) {
const id = league.id.toString(); const id = league.id.toString();
const name = league.name.toString(); const name = league.name.toString();
leagueMap.set(id, name); leagueMap.set(id, name);
@@ -39,7 +19,7 @@ export class GetAllRacesPresenter {
} }
this.model = { this.model = {
races: output.races.map(race => ({ races: result.races.map(race => ({
id: race.id, id: race.id,
track: race.track, track: race.track,
car: race.car, car: race.car,

View File

@@ -1,7 +1,7 @@
import type { IdentitySessionPort } from '../ports/IdentitySessionPort'; import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
import { Result } from '@core/shared/application/Result'; import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; 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 = {}; export type LogoutInput = {};
@@ -13,7 +13,7 @@ export type LogoutErrorCode = 'REPOSITORY_ERROR';
export type LogoutApplicationError = ApplicationErrorCode<LogoutErrorCode, { message: string }>; export type LogoutApplicationError = ApplicationErrorCode<LogoutErrorCode, { message: string }>;
export class LogoutUseCase { export class LogoutUseCase implements UseCase<LogoutInput, void, LogoutErrorCode> {
private readonly sessionPort: IdentitySessionPort; private readonly sessionPort: IdentitySessionPort;
constructor( constructor(
@@ -24,7 +24,7 @@ export class LogoutUseCase {
this.sessionPort = sessionPort; this.sessionPort = sessionPort;
} }
async execute(): Promise<Result<void, LogoutApplicationError>> { async execute(input: LogoutInput): Promise<Result<void, LogoutApplicationError>> {
try { try {
await this.sessionPort.clearSession(); await this.sessionPort.clearSession();

View File

@@ -58,8 +58,8 @@ describe('GetAllRacesUseCase', () => {
mockRaceRepo, mockRaceRepo,
mockLeagueRepo, mockLeagueRepo,
mockLogger, mockLogger,
output,
); );
useCase.setOutput(output);
const race1 = { const race1 = {
id: 'race1', id: 'race1',
@@ -100,8 +100,8 @@ describe('GetAllRacesUseCase', () => {
mockRaceRepo, mockRaceRepo,
mockLeagueRepo, mockLeagueRepo,
mockLogger, mockLogger,
output,
); );
useCase.setOutput(output);
mockRaceFindAll.mockResolvedValue([]); mockRaceFindAll.mockResolvedValue([]);
mockLeagueFindAll.mockResolvedValue([]); mockLeagueFindAll.mockResolvedValue([]);

View File

@@ -18,13 +18,18 @@ export interface GetAllRacesResult {
export type GetAllRacesErrorCode = 'REPOSITORY_ERROR'; export type GetAllRacesErrorCode = 'REPOSITORY_ERROR';
export class GetAllRacesUseCase { export class GetAllRacesUseCase {
private output: UseCaseOutputPort<GetAllRacesResult> | null = null;
constructor( constructor(
private readonly raceRepository: IRaceRepository, private readonly raceRepository: IRaceRepository,
private readonly leagueRepository: ILeagueRepository, private readonly leagueRepository: ILeagueRepository,
private readonly logger: Logger, private readonly logger: Logger,
private readonly output: UseCaseOutputPort<GetAllRacesResult>,
) {} ) {}
setOutput(output: UseCaseOutputPort<GetAllRacesResult>) {
this.output = output;
}
async execute( async execute(
_input: GetAllRacesInput, _input: GetAllRacesInput,
): Promise<Result<void, ApplicationErrorCode<GetAllRacesErrorCode, { message: string }>>> { ): Promise<Result<void, ApplicationErrorCode<GetAllRacesErrorCode, { message: string }>>> {
@@ -40,6 +45,9 @@ export class GetAllRacesUseCase {
}; };
this.logger.debug('Successfully retrieved all races.'); this.logger.debug('Successfully retrieved all races.');
if (!this.output) {
throw new Error('Output not set');
}
this.output.present(result); this.output.present(result);
return Result.ok(undefined); return Result.ok(undefined);
} catch (error) { } catch (error) {

View File

@@ -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 { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { Driver } from '../../domain/entities/Driver'; import type { Driver } from '../../domain/entities/Driver';
@@ -49,13 +49,14 @@ export class GetDriversLeaderboardUseCase {
private readonly driverStatsService: IDriverStatsService, private readonly driverStatsService: IDriverStatsService,
private readonly getDriverAvatar: (driverId: string) => Promise<string | undefined>, private readonly getDriverAvatar: (driverId: string) => Promise<string | undefined>,
private readonly logger: Logger, private readonly logger: Logger,
private readonly output: UseCaseOutputPort<GetDriversLeaderboardResult>,
) {} ) {}
async execute( async execute(
input: GetDriversLeaderboardInput, input: GetDriversLeaderboardInput,
): Promise< ): Promise<
Result< Result<
GetDriversLeaderboardResult, void,
ApplicationErrorCode<GetDriversLeaderboardErrorCode, { message: string }> ApplicationErrorCode<GetDriversLeaderboardErrorCode, { message: string }>
> >
> { > {
@@ -107,7 +108,9 @@ export class GetDriversLeaderboardUseCase {
this.logger.debug('Successfully computed drivers leaderboard'); this.logger.debug('Successfully computed drivers leaderboard');
return Result.ok(result); this.output.present(result);
return Result.ok(undefined);
} catch (error) { } catch (error) {
const err = error instanceof Error ? error : new Error(String(error)); const err = error instanceof Error ? error : new Error(String(error));

View File

@@ -11,9 +11,7 @@ export type UpdateDriverProfileInput = {
country?: string; country?: string;
}; };
export type UpdateDriverProfileResult = { export type UpdateDriverProfileResult = Driver;
driverId: string;
};
export type UpdateDriverProfileErrorCode = export type UpdateDriverProfileErrorCode =
| 'DRIVER_NOT_FOUND' | 'DRIVER_NOT_FOUND'
@@ -64,11 +62,7 @@ export class UpdateDriverProfileUseCase implements UseCase<UpdateDriverProfileIn
await this.driverRepository.update(updated); await this.driverRepository.update(updated);
const result: UpdateDriverProfileResult = { this.output.present(updated);
driverId: updated.id,
};
this.output.present(result);
return Result.ok(undefined); return Result.ok(undefined);
} catch (error) { } catch (error) {

View File

@@ -24,52 +24,52 @@ Directory: apps/api/src/domain/analytics
### Controllers ### Controllers
- File: apps/api/src/domain/analytics/AnalyticsController.ts - File: apps/api/src/domain/analytics/AnalyticsController.ts
- [ ] Review all controller methods and update them to consume response models returned from the analytics service rather than constructing or interpreting DTOs manually. - [x] Review all controller methods and update them to consume response models returned from the analytics service rather than constructing or interpreting DTOs manually.
- [ ] Ensure controller method signatures and return types reflect the new response model naming and structure introduced by presenters. - [x] Ensure controller method signatures and return types reflect the new response model naming and structure introduced by presenters.
- File: apps/api/src/domain/analytics/AnalyticsController.test.ts - File: apps/api/src/domain/analytics/AnalyticsController.test.ts
- [ ] Update tests to assert that controller methods receive response models from the service and do not depend on internal mapping logic inside the service. - [x] Update tests to assert that controller methods receive response models from the service and do not depend on internal mapping logic inside the service.
- [ ] Adjust expectations to align with response model terminology instead of any previous view model terminology. - [x] Adjust expectations to align with response model terminology instead of any previous view model terminology.
### Services ### Services
- File: apps/api/src/domain/analytics/AnalyticsService.ts - File: apps/api/src/domain/analytics/AnalyticsService.ts
- [ ] Identify each method that calls a core analytics use case and ensure it passes the use case result through the appropriate presenter via the output port. - [x] Identify each method that calls a core analytics use case and ensure it passes the use case result through the appropriate presenter via the output port.
- [ ] Remove any mapping or DTO-building logic from the service methods; move all such responsibilities into dedicated analytics presenters. - [x] Remove any mapping or DTO-building logic from the service methods; move all such responsibilities into dedicated analytics presenters.
- [ ] Ensure each service method returns only the presenters response model, not any core domain objects or intermediate data. - [x] Ensure each service method returns only the presenters response model, not any core domain objects or intermediate data.
- [ ] Verify that all injected dependencies are repositories and use cases injected via tokens, with no presenters injected via dependency injection. - [x] Verify that all injected dependencies are repositories and use cases injected via tokens, with no presenters injected via dependency injection.
### Module and providers ### Module and providers
- File: apps/api/src/domain/analytics/AnalyticsModule.ts - File: apps/api/src/domain/analytics/AnalyticsModule.ts
- [ ] Ensure the module declares analytics presenters as providers and binds them as implementations of the generic output port for the relevant use cases. - [x] Ensure the module declares analytics presenters as providers and binds them as implementations of the generic output port for the relevant use cases.
- [ ] Ensure that services and controllers are wired to use analytics use cases via tokens, not via direct class references. - [x] Ensure that services and controllers are wired to use analytics use cases via tokens, not via direct class references.
- File: apps/api/src/domain/analytics/AnalyticsModule.test.ts - File: apps/api/src/domain/analytics/AnalyticsModule.test.ts
- [ ] Update module-level tests to reflect the new provider wiring, especially the binding of presenters as output ports for use cases. - [x] Update module-level tests to reflect the new provider wiring, especially the binding of presenters as output ports for use cases.
- File: apps/api/src/domain/analytics/AnalyticsProviders.ts - File: apps/api/src/domain/analytics/AnalyticsProviders.ts
- [ ] Ensure all analytics repositories and use cases are exposed via clear token constants and that these tokens are used consistently in service constructors. - [x] Ensure all analytics repositories and use cases are exposed via clear token constants and that these tokens are used consistently in service constructors.
- [ ] Add or adjust any tokens required for use case output port injection, without introducing presenter tokens for services. - [x] Add or adjust any tokens required for use case output port injection, without introducing presenter tokens for services.
### DTOs ### DTOs
- Files: apps/api/src/domain/analytics/dtos/GetAnalyticsMetricsOutputDTO.ts, GetDashboardDataOutputDTO.ts, RecordEngagementInputDTO.ts, RecordEngagementOutputDTO.ts, RecordPageViewInputDTO.ts, RecordPageViewOutputDTO.ts - Files: apps/api/src/domain/analytics/dtos/GetAnalyticsMetricsOutputDTO.ts, GetDashboardDataOutputDTO.ts, RecordEngagementInputDTO.ts, RecordEngagementOutputDTO.ts, RecordPageViewInputDTO.ts, RecordPageViewOutputDTO.ts
- [ ] Verify that all analytics DTOs represent API-level input or response models only and are not used directly inside core use cases. - [x] Verify that all analytics DTOs represent API-level input or response models only and are not used directly inside core use cases.
- [ ] Ensure naming reflects response model terminology where applicable and is consistent with presenters. - [x] Ensure naming reflects response model terminology where applicable and is consistent with presenters.
### Presenters ### Presenters
- Files: apps/api/src/domain/analytics/presenters/GetAnalyticsMetricsPresenter.ts, GetDashboardDataPresenter.ts, RecordEngagementPresenter.ts, RecordPageViewPresenter.ts - Files: apps/api/src/domain/analytics/presenters/GetAnalyticsMetricsPresenter.ts, GetDashboardDataPresenter.ts, RecordEngagementPresenter.ts, RecordPageViewPresenter.ts
- [ ] For each presenter, ensure it implements the use case output port contract for the corresponding analytics result model. - [x] For each presenter, ensure it implements the use case output port contract for the corresponding analytics result model.
- [ ] Ensure each presenter maintains internal response model state that is constructed from the core result model. - [x] Ensure each presenter maintains internal response model state that is constructed from the core result model.
- [ ] Ensure each presenter exposes a getter that returns the response model used by controllers or services. - [x] Ensure each presenter exposes a getter that returns the response model used by controllers or services.
- [ ] Move any analytics-specific mapping and transformation from services into these presenters. - [x] Move any analytics-specific mapping and transformation from services into these presenters.
- [ ] Align terminology within presenters to use response model rather than view model. - [x] Align terminology within presenters to use response model rather than view model.
- Files: apps/api/src/domain/analytics/presenters/GetAnalyticsMetricsPresenter.test.ts, GetDashboardDataPresenter.test.ts, RecordEngagementPresenter.test.ts, RecordPageViewPresenter.test.ts - Files: apps/api/src/domain/analytics/presenters/GetAnalyticsMetricsPresenter.test.ts, GetDashboardDataPresenter.test.ts, RecordEngagementPresenter.test.ts, RecordPageViewPresenter.test.ts
- [ ] Update tests to validate that each presenter receives a core result model, transforms it correctly, and exposes the correct response model. - [x] Update tests to validate that each presenter receives a core result model, transforms it correctly, and exposes the correct response model.
- [ ] Ensure tests no longer assume mapping occurs in services; all mapping assertions should target presenter behavior. - [x] Ensure tests no longer assume mapping occurs in services; all mapping assertions should target presenter behavior.
--- ---
@@ -80,51 +80,51 @@ Directory: apps/api/src/domain/auth
### Controllers ### Controllers
- File: apps/api/src/domain/auth/AuthController.ts - File: apps/api/src/domain/auth/AuthController.ts
- [ ] Review all controller methods and ensure they consume response models returned from the auth service, not raw domain objects or DTOs assembled by the controller. - [x] Review all controller methods and ensure they consume response models returned from the auth service, not raw domain objects or DTOs assembled by the controller.
- [ ] Align controller return types with the response models produced by auth presenters. - [x] Align controller return types with the response models produced by auth presenters.
- File: apps/api/src/domain/auth/AuthController.test.ts - File: apps/api/src/domain/auth/AuthController.test.ts
- [ ] Update tests so they verify the controllers interaction with the auth service in terms of response models and error handling consistent with use case Results. - [x] Update tests so they verify the controllers interaction with the auth service in terms of response models and error handling consistent with use case Results.
### Services ### Services
- File: apps/api/src/domain/auth/AuthService.ts - File: apps/api/src/domain/auth/AuthService.ts
- [ ] For signup, login, and logout operations, ensure the service only coordinates input, calls the corresponding core use cases, and retrieves response models from auth presenters. - [x] For signup, login, and logout operations, ensure the service only coordinates input, calls the corresponding core use cases, and retrieves response models from auth presenters.
- [ ] Remove all mapping logic in the service that translates between core user or session representations and API DTOs; move this logic into dedicated presenters. - [x] Remove all mapping logic in the service that translates between core user or session representations and API DTOs; move this logic into dedicated presenters.
- [ ] Ensure use cases are injected via tokens and that repositories and ports also use token-based injection. - [x] Ensure use cases are injected via tokens and that repositories and ports also use token-based injection.
- [ ] Ensure presenters are not injected into the service via dependency injection and are instead treated as part of the output port wiring and imported where necessary. - [x] Ensure presenters are not injected into the service via dependency injection and are instead treated as part of the output port wiring and imported where necessary.
- [ ] Ensure each public service method returns a response model based on presenter state, not core domain entities. - [x] Ensure each public service method returns a response model based on presenter state, not core domain entities.
### Module and providers ### Module and providers
- File: apps/api/src/domain/auth/AuthModule.ts - File: apps/api/src/domain/auth/AuthModule.ts
- [ ] Ensure the module declares auth presenters as providers and wires them as implementations of the use case output port for login, signup, and logout use cases. - [x] Ensure the module declares auth presenters as providers and wires them as implementations of the use case output port for login, signup, and logout use cases.
- [ ] Confirm that the auth service and controller depend on use cases and ports via the defined tokens. - [x] Confirm that the auth service and controller depend on use cases and ports via the defined tokens.
- File: apps/api/src/domain/auth/AuthModule.test.ts - File: apps/api/src/domain/auth/AuthModule.test.ts
- [ ] Update module tests to reflect the new wiring of auth presenters as output ports and the absence of presenter injection into services. - [x] Update module tests to reflect the new wiring of auth presenters as output ports and the absence of presenter injection into services.
- File: apps/api/src/domain/auth/AuthProviders.ts - File: apps/api/src/domain/auth/AuthProviders.ts
- [ ] Verify that all tokens for auth repositories, services, and use cases are defined and consistently used. - [x] Verify that all tokens for auth repositories, services, and use cases are defined and consistently used.
- [ ] Add or adjust tokens required for output port injection, ensuring presenters themselves are not injected into services. - [x] Add or adjust tokens required for output port injection, ensuring presenters themselves are not injected into services.
### DTOs ### DTOs
- File: apps/api/src/domain/auth/dtos/AuthDto.ts - File: apps/api/src/domain/auth/dtos/AuthDto.ts
- [ ] Ensure DTOs in this file represent API-level input and response models only and are not referenced by core use cases. - [x] Ensure DTOs in this file represent API-level input and response models only and are not referenced by core use cases.
- [ ] Align DTO naming with response model terminology where applicable. - [x] Align DTO naming with response model terminology where applicable.
### Presenters ### Presenters
- Files: apps/api/src/domain/auth/presenters/AuthSessionPresenter.ts, CommandResultPresenter.ts - Files: apps/api/src/domain/auth/presenters/AuthSessionPresenter.ts, CommandResultPresenter.ts
- [ ] Ensure each presenter implements the generic use case output port contract for the relevant auth result model. - [x] Ensure each presenter implements the generic use case output port contract for the relevant auth result model.
- [ ] Ensure each presenter maintains internal response model state derived from the core result model. - [x] Ensure each presenter maintains internal response model state derived from the core result model.
- [ ] Ensure a getter method is available to expose the response model to controllers and services. - [x] Ensure a getter method is available to expose the response model to controllers and services.
- [ ] Move all auth-related mapping logic from the auth service into these presenters. - [x] Move all auth-related mapping logic from the auth service into these presenters.
- [ ] Normalize terminology within presenters to use response model instead of view model. - [x] Normalize terminology within presenters to use response model instead of view model.
- File: apps/api/src/domain/auth/presenters/AuthSessionPresenter.test.ts - File: apps/api/src/domain/auth/presenters/AuthSessionPresenter.test.ts
- [ ] Update tests so they validate that the auth session presenter receives core result models from auth use cases and correctly transforms them into auth response models. - [x] Update tests so they validate that the auth session presenter receives core result models from auth use cases and correctly transforms them into auth response models.
--- ---
@@ -135,43 +135,43 @@ Directory: apps/api/src/domain/dashboard
### Controllers ### Controllers
- File: apps/api/src/domain/dashboard/DashboardController.ts - File: apps/api/src/domain/dashboard/DashboardController.ts
- [ ] Ensure controller methods depend on dashboard service methods that return response models, not core objects or partial mappings. - [x] Ensure controller methods depend on dashboard service methods that return response models, not core objects or partial mappings.
- [ ] Align method return types and expectations with the dashboard response models built by presenters. - [x] Align method return types and expectations with the dashboard response models built by presenters.
- File: apps/api/src/domain/dashboard/DashboardController.test.ts - File: apps/api/src/domain/dashboard/DashboardController.test.ts
- [ ] Update tests to assert that the controller interacts with the service in terms of response models, not internal mapping behavior. - [x] Update tests to assert that the controller interacts with the service in terms of response models, not internal mapping behavior.
### Services ### Services
- File: apps/api/src/domain/dashboard/DashboardService.ts - File: apps/api/src/domain/dashboard/DashboardService.ts
- [ ] Identify all dashboard service methods that construct or manipulate DTOs directly and move this logic into dashboard presenters. - [x] Identify all dashboard service methods that construct or manipulate DTOs directly and move this logic into dashboard presenters.
- [ ] Ensure each service method calls the appropriate dashboard use case, allows it to drive presenters through output ports, and returns a response model obtained from presenters. - [x] Ensure each service method calls the appropriate dashboard use case, allows it to drive presenters through output ports, and returns a response model obtained from presenters.
- [ ] Confirm that dashboard use cases and repositories are injected via tokens, with no presenters injected via dependency injection. - [x] Confirm that dashboard use cases and repositories are injected via tokens, with no presenters injected via dependency injection.
### Module and providers ### Module and providers
- File: apps/api/src/domain/dashboard/DashboardModule.ts - File: apps/api/src/domain/dashboard/DashboardModule.ts
- [ ] Ensure the module binds dashboard presenters as output port implementations for the relevant use cases. - [x] Ensure the module binds dashboard presenters as output port implementations for the relevant use cases.
- [ ] Ensure dashboard services depend on use cases via tokens only. - [x] Ensure dashboard services depend on use cases via tokens only.
- File: apps/api/src/domain/dashboard/DashboardModule.test.ts - File: apps/api/src/domain/dashboard/DashboardModule.test.ts
- [ ] Adjust tests to confirm correct provider wiring of presenters as output ports. - [x] Adjust tests to confirm correct provider wiring of presenters as output ports.
- File: apps/api/src/domain/dashboard/DashboardProviders.ts - File: apps/api/src/domain/dashboard/DashboardProviders.ts
- [ ] Review token definitions for repositories, services, and use cases; ensure all are used consistently in constructor injection. - [x] Review token definitions for repositories, services, and use cases; ensure all are used consistently in constructor injection.
- [ ] Add or adjust any tokens needed for output port wiring. - [x] Add or adjust any tokens needed for output port wiring.
### DTOs ### DTOs
- Files: apps/api/src/domain/dashboard/dtos/DashboardDriverSummaryDTO.ts, DashboardFeedItemSummaryDTO.ts, DashboardRaceSummaryDTO.ts - Files: apps/api/src/domain/dashboard/dtos/DashboardDriverSummaryDTO.ts, DashboardFeedItemSummaryDTO.ts, DashboardRaceSummaryDTO.ts
- [ ] Verify that these DTOs are used only as API-level response models from presenters or services and not within core use cases. - [x] Verify that these DTOs are used only as API-level response models from presenters or services and not within core use cases.
- [ ] Align naming and fields with the response models produced by dashboard presenters. - [x] Align naming and fields with the response models produced by dashboard presenters.
### Presenters ### Presenters
- (Any dashboard presenters, when added or identified in the codebase) - (Any dashboard presenters, when added or identified in the codebase)
- [ ] Ensure they implement the use case output port contract, maintain internal response model state, and expose response model getters. - [x] Ensure they implement the use case output port contract, maintain internal response model state, and expose response model getters.
- [ ] Move all dashboard mapping and DTO-building logic into these presenters. - [x] Move all dashboard mapping and DTO-building logic into these presenters.
--- ---
@@ -182,46 +182,46 @@ Directory: apps/api/src/domain/driver
### Controllers ### Controllers
- File: apps/api/src/domain/driver/DriverController.ts - File: apps/api/src/domain/driver/DriverController.ts
- [ ] Ensure controller methods depend on driver service methods that return response models, not domain entities or partial DTOs. - [x] Ensure controller methods depend on driver service methods that return response models, not domain entities or partial DTOs.
- [ ] Align method signatures and return types with driver response models provided by presenters. - [x] Align method signatures and return types with driver response models provided by presenters.
- File: apps/api/src/domain/driver/DriverController.test.ts - File: apps/api/src/domain/driver/DriverController.test.ts
- [ ] Update tests so they verify controller interactions with the driver service via response models and error handling consistent with use case Results. - [x] Update tests so they verify controller interactions with the driver service via response models and error handling consistent with use case Results.
### Services ### Services
- File: apps/api/src/domain/driver/DriverService.ts - File: apps/api/src/domain/driver/DriverService.ts
- [ ] Identify all mapping logic from driver domain objects to DTOs in the service and move that logic into driver presenters. - [x] Identify all mapping logic from driver domain objects to DTOs in the service and move that logic into driver presenters.
- [ ] Ensure each service method calls the relevant driver use case, lets the use case present results through presenters, and returns response models obtained from presenters. - [x] Ensure each service method calls the relevant driver use case, lets the use case present results through presenters, and returns response models obtained from presenters.
- [ ] Confirm that repositories and use cases are injected via tokens, not via direct class references. - [x] Confirm that repositories and use cases are injected via tokens, not via direct class references.
- [ ] Ensure no presenter is injected into the driver service via dependency injection. - [x] Ensure no presenter is injected into the driver service via dependency injection.
### Module and providers ### Module and providers
- File: apps/api/src/domain/driver/DriverModule.ts - File: apps/api/src/domain/driver/DriverModule.ts
- [ ] Ensure driver presenters are registered as providers and are bound as output port implementations for driver use cases. - [x] Ensure driver presenters are registered as providers and are bound as output port implementations for driver use cases.
- [ ] Ensure the driver service and controller depend on use cases via tokens. - [x] Ensure the driver service and controller depend on use cases via tokens.
- File: apps/api/src/domain/driver/DriverModule.test.ts - File: apps/api/src/domain/driver/DriverModule.test.ts
- [ ] Update module tests to reflect the wiring of presenters as output ports and the token-based injection of use cases. - [x] Update module tests to reflect the wiring of presenters as output ports and the token-based injection of use cases.
### DTOs ### DTOs
- Files: apps/api/src/domain/driver/dtos/CompleteOnboardingInputDTO.ts, CompleteOnboardingOutputDTO.ts, DriverDTO.ts, DriverLeaderboardItemDTO.ts, DriverRegistrationStatusDTO.ts, DriversLeaderboardDTO.ts, DriverStatsDTO.ts, GetDriverOutputDTO.ts, GetDriverProfileOutputDTO.ts, GetDriverRegistrationStatusQueryDTO.ts - Files: apps/api/src/domain/driver/dtos/CompleteOnboardingInputDTO.ts, CompleteOnboardingOutputDTO.ts, DriverDTO.ts, DriverLeaderboardItemDTO.ts, DriverRegistrationStatusDTO.ts, DriversLeaderboardDTO.ts, DriverStatsDTO.ts, GetDriverOutputDTO.ts, GetDriverProfileOutputDTO.ts, GetDriverRegistrationStatusQueryDTO.ts
- [ ] Ensure these DTOs are used exclusively as API input or response models, and not inside core use cases. - [x] Ensure these DTOs are used exclusively as API input or response models, and not inside core use cases.
- [ ] Align names and shapes with the response models and input expectations defined in driver presenters and services. - [x] Align names and shapes with the response models and input expectations defined in driver presenters and services.
### Presenters ### Presenters
- Files: apps/api/src/domain/driver/presenters/CompleteOnboardingPresenter.ts, DriverPresenter.ts, DriverProfilePresenter.ts, DriverRegistrationStatusPresenter.ts, DriversLeaderboardPresenter.ts, DriverStatsPresenter.ts - Files: apps/api/src/domain/driver/presenters/CompleteOnboardingPresenter.ts, DriverPresenter.ts, DriverProfilePresenter.ts, DriverRegistrationStatusPresenter.ts, DriversLeaderboardPresenter.ts, DriverStatsPresenter.ts
- [ ] Ensure each presenter implements the use case output port contract for its driver result model. - [x] Ensure each presenter implements the use case output port contract for its driver result model.
- [ ] Ensure each presenter maintains an internal response model that is constructed from the driver result model. - [x] Ensure each presenter maintains an internal response model that is constructed from the driver result model.
- [ ] Ensure each presenter exposes a getter that returns the response model. - [x] Ensure each presenter exposes a getter that returns the response model.
- [ ] Move all driver mapping logic from the driver service into the relevant presenters. - [x] Move all driver mapping logic from the driver service into the relevant presenters.
- [ ] Consistently use response model terminology within presenters. - [x] Consistently use response model terminology within presenters.
- Files: apps/api/src/domain/driver/presenters/DriverRegistrationStatusPresenter.test.ts, DriversLeaderboardPresenter.test.ts, DriverStatsPresenter.test.ts - Files: apps/api/src/domain/driver/presenters/DriverRegistrationStatusPresenter.test.ts, DriversLeaderboardPresenter.test.ts, DriverStatsPresenter.test.ts
- [ ] Update tests to confirm that presenters correctly transform core result models into driver response models and that no mapping remains in the service. - [x] Update tests to confirm that presenters correctly transform core result models into driver response models and that no mapping remains in the service.
--- ---
@@ -278,48 +278,48 @@ Directory: apps/api/src/domain/media
### Controllers ### Controllers
- File: apps/api/src/domain/media/MediaController.ts - File: apps/api/src/domain/media/MediaController.ts
- [ ] Ensure controller methods depend on media service methods that return response models, not core media objects or partial DTOs. - [x] Ensure controller methods depend on media service methods that return response models, not core media objects or partial DTOs.
- [ ] Align the controller return types with media response models produced by presenters. - [x] Align the controller return types with media response models produced by presenters.
- File: apps/api/src/domain/media/MediaController.test.ts - File: apps/api/src/domain/media/MediaController.test.ts
- [ ] Update tests to verify that controllers work with media response models returned from the service. - [x] Update tests to verify that controllers work with media response models returned from the service.
### Services ### Services
- File: apps/api/src/domain/media/MediaService.ts - File: apps/api/src/domain/media/MediaService.ts
- [ ] Identify all mapping from media domain objects to DTOs and move this logic into media presenters. - [x] Identify all mapping from media domain objects to DTOs and move this logic into media presenters.
- [ ] Ensure each service method calls the relevant media use case, allows it to use presenters via output ports, and returns response models from presenters. - [x] Ensure each service method calls the relevant media use case, allows it to use presenters via output ports, and returns response models from presenters.
- [ ] Confirm that repositories and use cases are injected via tokens and that no presenter is injected via dependency injection. - [x] Confirm that repositories and use cases are injected via tokens and that no presenter is injected via dependency injection.
### Module and providers ### Module and providers
- File: apps/api/src/domain/media/MediaModule.ts - File: apps/api/src/domain/media/MediaModule.ts
- [ ] Ensure media presenters are registered as providers and bound as output port implementations for media use cases. - [x] Ensure media presenters are registered as providers and bound as output port implementations for media use cases.
- File: apps/api/src/domain/media/MediaModule.test.ts - File: apps/api/src/domain/media/MediaModule.test.ts
- [ ] Update tests to reflect the correct provider wiring and output port bindings. - [x] Update tests to reflect the correct provider wiring and output port bindings.
- File: apps/api/src/domain/media/MediaProviders.ts - File: apps/api/src/domain/media/MediaProviders.ts
- [ ] Review token definitions for repositories and use cases; ensure they are used consistently for constructor injection. - [x] Review token definitions for repositories and use cases; ensure they are used consistently for constructor injection.
- [ ] Add or adjust tokens required for output port wiring without introducing presenter tokens for services. - [x] Add or adjust tokens required for output port wiring without introducing presenter tokens for services.
### DTOs ### DTOs
- Files: apps/api/src/domain/media/dtos/DeleteMediaOutputDTO.ts, GetAvatarOutputDTO.ts, GetMediaOutputDTO.ts, RequestAvatarGenerationInputDTO.ts, RequestAvatarGenerationOutputDTO.ts, UpdateAvatarInputDTO.ts, UpdateAvatarOutputDTO.ts, UploadMediaInputDTO.ts, UploadMediaOutputDTO.ts - Files: apps/api/src/domain/media/dtos/DeleteMediaOutputDTO.ts, GetAvatarOutputDTO.ts, GetMediaOutputDTO.ts, RequestAvatarGenerationInputDTO.ts, RequestAvatarGenerationOutputDTO.ts, UpdateAvatarInputDTO.ts, UpdateAvatarOutputDTO.ts, UploadMediaInputDTO.ts, UploadMediaOutputDTO.ts
- [ ] Ensure these DTOs serve only as API input and response models and are not used directly within core use cases. - [x] Ensure these DTOs serve only as API input and response models and are not used directly within core use cases.
- [ ] Align naming and structure with the response models built by media presenters. - [x] Align naming and structure with the response models built by media presenters.
### Presenters ### Presenters
- Files: apps/api/src/domain/media/presenters/DeleteMediaPresenter.ts, GetAvatarPresenter.ts, GetMediaPresenter.ts, RequestAvatarGenerationPresenter.ts, UpdateAvatarPresenter.ts, UploadMediaPresenter.ts - Files: apps/api/src/domain/media/presenters/DeleteMediaPresenter.ts, GetAvatarPresenter.ts, GetMediaPresenter.ts, RequestAvatarGenerationPresenter.ts, UpdateAvatarPresenter.ts, UploadMediaPresenter.ts
- [ ] Ensure each presenter implements the use case output port contract for its media result model. - [x] Ensure each presenter implements the use case output port contract for its media result model.
- [ ] Ensure each presenter maintains internal response model state derived from the core result model and exposes a getter. - [x] Ensure each presenter maintains internal response model state derived from the core result model and exposes a getter.
- [ ] Move all mapping and response model construction from the media service into these presenters. - [x] Move all mapping and response model construction from the media service into these presenters.
### Types ### Types
- Files: apps/api/src/domain/media/types/FacePhotoData.ts, SuitColor.ts - Files: apps/api/src/domain/media/types/FacePhotoData.ts, SuitColor.ts
- [ ] Verify that these types are used appropriately as part of input or response models and not as replacements for core domain entities inside use cases. - [x] Verify that these types are used appropriately as part of input or response models and not as replacements for core domain entities inside use cases.
--- ---
@@ -330,32 +330,32 @@ Directory: apps/api/src/domain/payments
### Controllers ### Controllers
- File: apps/api/src/domain/payments/PaymentsController.ts - File: apps/api/src/domain/payments/PaymentsController.ts
- [ ] Ensure controller methods call payments use cases via services or directly, receive results that are presented via presenters, and return payments response models. - [x] Ensure controller methods call payments use cases via services or directly, receive results that are presented via presenters, and return payments response models.
- [ ] Remove any mapping logic from the controller and rely exclusively on presenters for transforming result models into response models. - [x] Remove any mapping logic from the controller and rely exclusively on presenters for transforming result models into response models.
### Module and providers ### Module and providers
- File: apps/api/src/domain/payments/PaymentsModule.ts - File: apps/api/src/domain/payments/PaymentsModule.ts
- [ ] Ensure payments presenters are registered as providers and bound as output port implementations for payments use cases. - [x] Ensure payments presenters are registered as providers and bound as output port implementations for payments use cases.
- File: apps/api/src/domain/payments/PaymentsModule.test.ts - File: apps/api/src/domain/payments/PaymentsModule.test.ts
- [ ] Update module tests to reflect correct output port wiring and token-based use case injection. - [x] Update module tests to reflect correct output port wiring and token-based use case injection.
- File: apps/api/src/domain/payments/PaymentsProviders.ts - File: apps/api/src/domain/payments/PaymentsProviders.ts
- [ ] Review token definitions for payments repositories and use cases; ensure they are consistently used for dependency injection. - [x] Review token definitions for payments repositories and use cases; ensure they are consistently used for dependency injection.
- [ ] Add or adjust tokens as needed for output port wiring. - [x] Add or adjust tokens as needed for output port wiring.
### DTOs ### DTOs
- Files: apps/api/src/domain/payments/dtos/CreatePaymentInputDTO.ts, CreatePaymentOutputDTO.ts, MemberPaymentStatus.ts, MembershipFeeType.ts, PayerType.ts, PaymentDTO.ts, PaymentsDto.ts, PaymentStatus.ts, PaymentType.ts, PrizeType.ts, ReferenceType.ts, TransactionType.ts, UpdatePaymentStatusInputDTO.ts, UpdatePaymentStatusOutputDTO.ts - Files: apps/api/src/domain/payments/dtos/CreatePaymentInputDTO.ts, CreatePaymentOutputDTO.ts, MemberPaymentStatus.ts, MembershipFeeType.ts, PayerType.ts, PaymentDTO.ts, PaymentsDto.ts, PaymentStatus.ts, PaymentType.ts, PrizeType.ts, ReferenceType.ts, TransactionType.ts, UpdatePaymentStatusInputDTO.ts, UpdatePaymentStatusOutputDTO.ts
- [ ] Ensure these DTOs are used solely as API-level input and response models and not within core payments use cases. - [x] Ensure these DTOs are used solely as API-level input and response models and not within core payments use cases.
- [ ] Align naming and structure with the response models and inputs expected by payments presenters and services. - [x] Align naming and structure with the response models and inputs expected by payments presenters and services.
### Presenters ### Presenters
- (Any payments presenters, once identified or added during implementation) - (Any payments presenters, once identified or added during implementation)
- [ ] Ensure they implement the use case output port contract, maintain internal response model state, and expose getters for response models. - [x] Ensure they implement the use case output port contract, maintain internal response model state, and expose getters for response models.
- [ ] Centralize all payments mapping logic in these presenters. - [x] Centralize all payments mapping logic in these presenters.
--- ---
@@ -403,50 +403,50 @@ Directory: apps/api/src/domain/race
### Controllers ### Controllers
- File: apps/api/src/domain/race/RaceController.ts - File: apps/api/src/domain/race/RaceController.ts
- [ ] Ensure controller methods call race service methods that return response models and do not perform any mapping from race domain entities. - [x] Ensure controller methods call race service methods that return response models and do not perform any mapping from race domain entities.
- [ ] Adjust controller return types to reflect race response models created by presenters. - [x] Adjust controller return types to reflect race response models created by presenters.
- File: apps/api/src/domain/race/RaceController.test.ts - File: apps/api/src/domain/race/RaceController.test.ts
- [ ] Update tests so they verify controller behavior in terms of response models and error handling based on use case Results. - [x] Update tests so they verify controller behavior in terms of response models and error handling based on use case Results.
### Services ### Services
- File: apps/api/src/domain/race/RaceService.ts - File: apps/api/src/domain/race/RaceService.ts
- [ ] Identify all mapping logic from race domain entities to DTOs and move it into race presenters. - [x] Identify all mapping logic from race domain entities to DTOs and move it into race presenters.
- [ ] Ensure each service method calls the relevant race use case, lets it present through race presenters, and returns response models from presenters. - [x] Ensure each service method calls the relevant race use case, lets it present through race presenters, and returns response models from presenters.
- [ ] Confirm race repositories and use cases are injected via tokens only and that no presenter is injected via dependency injection. - [x] Confirm race repositories and use cases are injected via tokens only and that no presenter is injected via dependency injection.
- File: apps/api/src/domain/race/RaceService.test.ts - File: apps/api/src/domain/race/RaceService.test.ts
- [ ] Update tests to reflect that the race service now delegates mapping to presenters and returns response models. - [x] Update tests to reflect that the race service now delegates mapping to presenters and returns response models.
### Module and providers ### Module and providers
- File: apps/api/src/domain/race/RaceModule.ts - File: apps/api/src/domain/race/RaceModule.ts
- [ ] Ensure race presenters are registered as providers and bound as output port implementations for race use cases. - [x] Ensure race presenters are registered as providers and bound as output port implementations for race use cases.
- File: apps/api/src/domain/race/RaceModule.test.ts - File: apps/api/src/domain/race/RaceModule.test.ts
- [ ] Update tests to confirm correct wiring of race presenters and token-based use case injection. - [x] Update tests to confirm correct wiring of race presenters and token-based use case injection.
- File: apps/api/src/domain/race/RaceProviders.ts - File: apps/api/src/domain/race/RaceProviders.ts
- [ ] Verify token definitions for race repositories and use cases and ensure consistent usage. - [x] Verify token definitions for race repositories and use cases and ensure consistent usage.
- [ ] Add or adjust tokens to support output port wiring. - [x] Add or adjust tokens to support output port wiring.
### DTOs ### DTOs
- Files: apps/api/src/domain/race/dtos/AllRacesPageDTO.ts, DashboardDriverSummaryDTO.ts, DashboardFeedSummaryDTO.ts, DashboardFriendSummaryDTO.ts, DashboardLeagueStandingSummaryDTO.ts, DashboardOverviewDTO.ts, DashboardRaceSummaryDTO.ts, DashboardRecentResultDTO.ts, FileProtestCommandDTO.ts, GetRaceDetailParamsDTO.ts, ImportRaceResultsDTO.ts, ImportRaceResultsSummaryDTO.ts, QuickPenaltyCommandDTO.ts, RaceActionParamsDTO.ts, RaceDetailDTO.ts, RaceDetailEntryDTO.ts, RaceDetailLeagueDTO.ts, RaceDetailRaceDTO.ts, RaceDetailRegistrationDTO.ts, RaceDetailUserResultDTO.ts, RacePenaltiesDTO.ts, RacePenaltyDTO.ts, RaceProtestDTO.ts, RaceProtestsDTO.ts, RaceResultDTO.ts, RaceResultsDetailDTO.ts, RacesPageDataDTO.ts, RacesPageDataRaceDTO.ts, RaceStatsDTO.ts, RaceWithSOFDTO.ts, RegisterForRaceParamsDTO.ts, RequestProtestDefenseCommandDTO.ts, ReviewProtestCommandDTO.ts, WithdrawFromRaceParamsDTO.ts - Files: apps/api/src/domain/race/dtos/AllRacesPageDTO.ts, DashboardDriverSummaryDTO.ts, DashboardFeedSummaryDTO.ts, DashboardFriendSummaryDTO.ts, DashboardLeagueStandingSummaryDTO.ts, DashboardOverviewDTO.ts, DashboardRaceSummaryDTO.ts, DashboardRecentResultDTO.ts, FileProtestCommandDTO.ts, GetRaceDetailParamsDTO.ts, ImportRaceResultsDTO.ts, ImportRaceResultsSummaryDTO.ts, QuickPenaltyCommandDTO.ts, RaceActionParamsDTO.ts, RaceDetailDTO.ts, RaceDetailEntryDTO.ts, RaceDetailLeagueDTO.ts, RaceDetailRaceDTO.ts, RaceDetailRegistrationDTO.ts, RaceDetailUserResultDTO.ts, RacePenaltiesDTO.ts, RacePenaltyDTO.ts, RaceProtestDTO.ts, RaceProtestsDTO.ts, RaceResultDTO.ts, RaceResultsDetailDTO.ts, RacesPageDataDTO.ts, RacesPageDataRaceDTO.ts, RaceStatsDTO.ts, RaceWithSOFDTO.ts, RegisterForRaceParamsDTO.ts, RequestProtestDefenseCommandDTO.ts, ReviewProtestCommandDTO.ts, WithdrawFromRaceParamsDTO.ts
- [ ] Ensure these DTOs serve exclusively as API-level input and response models and are not used directly by core race use cases. - [x] Ensure these DTOs serve exclusively as API-level input and response models and are not used directly by core race use cases.
- [ ] Align naming and structures with race response models produced by presenters. - [x] Align naming and structures with race response models produced by presenters.
### Presenters ### Presenters
- Files: apps/api/src/domain/race/presenters/AllRacesPageDataPresenter.ts, GetAllRacesPresenter.ts, GetTotalRacesPresenter.ts, ImportRaceResultsApiPresenter.ts, RaceDetailPresenter.ts, RacePenaltiesPresenter.ts, RaceProtestsPresenter.ts, RaceWithSOFPresenter.ts - Files: apps/api/src/domain/race/presenters/AllRacesPageDataPresenter.ts, GetAllRacesPresenter.ts, GetTotalRacesPresenter.ts, ImportRaceResultsApiPresenter.ts, RaceDetailPresenter.ts, RacePenaltiesPresenter.ts, RaceProtestsPresenter.ts, RaceWithSOFPresenter.ts
- [ ] Ensure each race presenter implements the use case output port contract for its race result model. - [x] Ensure each race presenter implements the use case output port contract for its race result model.
- [ ] Ensure each presenter maintains internal response model state derived from core race result models and exposes getters. - [x] Ensure each presenter maintains internal response model state derived from core race result models and exposes getters.
- [ ] Move all race mapping logic and DTO construction from the race service into these presenters. - [x] Move all race mapping logic and DTO construction from the race service into these presenters.
- [ ] Use response model terminology consistently within presenters. - [x] Use response model terminology consistently within presenters.
- File: apps/api/src/domain/race/presenters/GetAllRacesPresenter.test.ts - File: apps/api/src/domain/race/presenters/GetAllRacesPresenter.test.ts
- [ ] Update tests so they validate presenter-based mapping from core race result models to race response models and reflect the absence of mapping logic in the service. - [x] Update tests so they validate presenter-based mapping from core race result models to race response models and reflect the absence of mapping logic in the service.
--- ---