tests
This commit is contained in:
@@ -10,6 +10,7 @@ import { HelloModule } from './domain/hello/HelloModule';
|
||||
import { LeagueModule } from './domain/league/LeagueModule';
|
||||
import { LoggingModule } from './domain/logging/LoggingModule';
|
||||
import { MediaModule } from './domain/media/MediaModule';
|
||||
import { NotificationsModule } from './domain/notifications/NotificationsModule';
|
||||
import { PaymentsModule } from './domain/payments/PaymentsModule';
|
||||
import { AdminModule } from './domain/admin/AdminModule';
|
||||
import { PolicyModule } from './domain/policy/PolicyModule';
|
||||
@@ -44,6 +45,7 @@ const ENABLE_BOOTSTRAP = getEnableBootstrap();
|
||||
SponsorModule,
|
||||
DriverModule,
|
||||
MediaModule,
|
||||
NotificationsModule,
|
||||
PaymentsModule,
|
||||
PolicyModule,
|
||||
AdminModule,
|
||||
|
||||
@@ -57,6 +57,7 @@ export class AnalyticsController {
|
||||
@Get('metrics')
|
||||
@ApiOperation({ summary: 'Get analytics metrics' })
|
||||
@ApiResponse({ status: 200, description: 'Analytics metrics', type: GetAnalyticsMetricsOutputDTO })
|
||||
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
||||
async getAnalyticsMetrics(): Promise<GetAnalyticsMetricsOutputDTO> {
|
||||
return this.analyticsService.getAnalyticsMetrics();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Controller, Get, Post, Body, Query, Inject, Res } from '@nestjs/common';
|
||||
import { Controller, Get, Post, Body, Query, Inject, Res, BadRequestException, UnauthorizedException } from '@nestjs/common';
|
||||
import { Public } from './Public';
|
||||
import { AuthService } from './AuthService';
|
||||
import { LoginParamsDTO, SignupParamsDTO, SignupSponsorParamsDTO, AuthSessionDTO, ForgotPasswordDTO, ResetPasswordDTO } from './dtos/AuthDto';
|
||||
@@ -12,7 +12,15 @@ export class AuthController {
|
||||
|
||||
@Post('signup')
|
||||
async signup(@Body() params: SignupParamsDTO): Promise<AuthSessionDTO> {
|
||||
return this.authService.signupWithEmail(params);
|
||||
try {
|
||||
return await this.authService.signupWithEmail(params);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
// Convert validation or business logic errors to BadRequest
|
||||
throw new BadRequestException(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@Post('signup-sponsor')
|
||||
@@ -22,7 +30,15 @@ export class AuthController {
|
||||
|
||||
@Post('login')
|
||||
async login(@Body() params: LoginParamsDTO): Promise<AuthSessionDTO> {
|
||||
return this.authService.loginWithEmail(params);
|
||||
try {
|
||||
return await this.authService.loginWithEmail(params);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
// Convert authentication errors to Unauthorized
|
||||
throw new UnauthorizedException(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@Get('session')
|
||||
|
||||
@@ -40,6 +40,10 @@ export class SignupParamsDTO {
|
||||
@MinLength(2)
|
||||
displayName!: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
username?: string; // Ignored, for compatibility with tests
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
iracingCustomerId?: string;
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ export class DashboardController {
|
||||
@ApiOperation({ summary: 'Get dashboard overview' })
|
||||
@ApiQuery({ name: 'driverId', description: 'Driver ID' })
|
||||
@ApiResponse({ status: 200, description: 'Dashboard overview', type: DashboardOverviewDTO })
|
||||
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
||||
async getDashboardOverview(
|
||||
@Query('driverId') _driverId: string,
|
||||
@Req() req: AuthenticatedRequest,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Body, Controller, Get, Param, Post, Put, Req, Inject } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Param, Post, Put, Req, Inject, NotFoundException } from '@nestjs/common';
|
||||
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
|
||||
import { Public } from '../auth/Public';
|
||||
@@ -80,8 +80,12 @@ export class DriverController {
|
||||
@ApiOperation({ summary: 'Get driver by ID' })
|
||||
@ApiResponse({ status: 200, description: 'Driver data', type: GetDriverOutputDTO })
|
||||
@ApiResponse({ status: 404, description: 'Driver not found' })
|
||||
async getDriver(@Param('driverId') driverId: string): Promise<GetDriverOutputDTO | null> {
|
||||
return await this.driverService.getDriver(driverId);
|
||||
async getDriver(@Param('driverId') driverId: string): Promise<GetDriverOutputDTO> {
|
||||
const result = await this.driverService.getDriver(driverId);
|
||||
if (!result) {
|
||||
throw new NotFoundException('Driver not found');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Public()
|
||||
@@ -90,7 +94,11 @@ export class DriverController {
|
||||
@ApiResponse({ status: 200, description: 'Driver profile data', type: GetDriverProfileOutputDTO })
|
||||
@ApiResponse({ status: 404, description: 'Driver not found' })
|
||||
async getDriverProfile(@Param('driverId') driverId: string): Promise<GetDriverProfileOutputDTO> {
|
||||
return await this.driverService.getDriverProfile(driverId);
|
||||
try {
|
||||
return await this.driverService.getDriverProfile(driverId);
|
||||
} catch (error) {
|
||||
throw new NotFoundException('Driver not found');
|
||||
}
|
||||
}
|
||||
|
||||
@Put(':driverId/profile')
|
||||
|
||||
@@ -75,6 +75,7 @@ export class DriverService {
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().details.message);
|
||||
}
|
||||
await this.driversLeaderboardPresenter!.present(result.unwrap());
|
||||
return this.driversLeaderboardPresenter!.getResponseModel();
|
||||
}
|
||||
|
||||
@@ -85,6 +86,7 @@ export class DriverService {
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().details.message);
|
||||
}
|
||||
this.driverStatsPresenter!.present(result.unwrap());
|
||||
return this.driverStatsPresenter!.getResponseModel();
|
||||
}
|
||||
|
||||
@@ -156,6 +158,9 @@ export class DriverService {
|
||||
this.logger.debug(`[DriverService] Fetching driver for driverId: ${driverId}`);
|
||||
|
||||
const driver = await this.driverRepository.findById(driverId);
|
||||
if (!driver) {
|
||||
return null;
|
||||
}
|
||||
await this.driverPresenter!.present(Result.ok(driver));
|
||||
return this.driverPresenter!.getResponseModel();
|
||||
}
|
||||
|
||||
@@ -14,8 +14,13 @@ export class HelloController {
|
||||
return { status: 'ok' };
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Get('hello')
|
||||
getHello() {
|
||||
return this.helloService.getHello();
|
||||
}
|
||||
|
||||
@Get()
|
||||
getRoot() {
|
||||
return this.helloService.getHello();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Body, Controller, Delete, Get, HttpCode, Param, Patch, Post, Inject, ValidationPipe, Query } from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, HttpCode, Param, Patch, Post, Inject, ValidationPipe, Query, NotFoundException, UnauthorizedException, BadRequestException } from '@nestjs/common';
|
||||
import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { Public } from '../auth/Public';
|
||||
import { LeagueService } from './LeagueService';
|
||||
@@ -63,6 +63,22 @@ export class LeagueController {
|
||||
return this.leagueService.getAllLeaguesWithCapacity();
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get('all')
|
||||
@ApiOperation({ summary: 'Get all leagues (alias for all-with-capacity)' })
|
||||
@ApiResponse({ status: 200, description: 'List of all leagues', type: AllLeaguesWithCapacityDTO })
|
||||
async getAllLeagues(): Promise<AllLeaguesWithCapacityDTO> {
|
||||
return this.leagueService.getAllLeaguesWithCapacity();
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get('available')
|
||||
@ApiOperation({ summary: 'Get available leagues (alias for all-with-capacity)' })
|
||||
@ApiResponse({ status: 200, description: 'List of available leagues', type: AllLeaguesWithCapacityDTO })
|
||||
async getAvailableLeagues(): Promise<AllLeaguesWithCapacityDTO> {
|
||||
return this.leagueService.getAllLeaguesWithCapacity();
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get('all-with-capacity-and-scoring')
|
||||
@ApiOperation({ summary: 'Get all leagues with capacity and scoring information' })
|
||||
@@ -148,6 +164,21 @@ export class LeagueController {
|
||||
return this.leagueService.updateLeagueMemberRole(leagueId, targetDriverId, input);
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get(':leagueId')
|
||||
@ApiOperation({ summary: 'Get league by ID' })
|
||||
@ApiResponse({ status: 200, description: 'League data', type: LeagueOwnerSummaryDTO })
|
||||
@ApiResponse({ status: 404, description: 'League not found' })
|
||||
async getLeague(@Param('leagueId') leagueId: string): Promise<LeagueOwnerSummaryDTO> {
|
||||
try {
|
||||
// For now, return basic league info - the service will handle 404
|
||||
const query: GetLeagueOwnerSummaryQueryDTO = { ownerId: 'unknown', leagueId };
|
||||
return await this.leagueService.getLeagueOwnerSummary(query);
|
||||
} catch (error) {
|
||||
throw new NotFoundException('League not found');
|
||||
}
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get(':leagueId/owner-summary/:ownerId')
|
||||
@ApiOperation({ summary: 'Get owner summary for a league' })
|
||||
@@ -458,8 +489,25 @@ export class LeagueController {
|
||||
@Post(':leagueId/join')
|
||||
@ApiOperation({ summary: 'Join a league (actor derived from session)' })
|
||||
@ApiResponse({ status: 200, description: 'Joined league successfully' })
|
||||
@ApiResponse({ status: 401, description: 'Unauthorized - no session' })
|
||||
@ApiResponse({ status: 400, description: 'Invalid league ID or other validation error' })
|
||||
async joinLeague(@Param('leagueId') leagueId: string) {
|
||||
return this.leagueService.joinLeague(leagueId);
|
||||
try {
|
||||
return await this.leagueService.joinLeague(leagueId);
|
||||
} catch (error) {
|
||||
// Handle authentication errors
|
||||
if (error instanceof Error && error.message === 'Unauthorized') {
|
||||
throw new UnauthorizedException('Authentication required');
|
||||
}
|
||||
|
||||
// Handle database/repository errors (invalid UUID, etc.)
|
||||
if (error instanceof Error && (error.message.includes('REPOSITORY_ERROR') || error.message.includes('invalid input syntax'))) {
|
||||
throw new BadRequestException('Invalid league ID');
|
||||
}
|
||||
|
||||
// Re-throw other errors
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@Post(':leagueId/transfer-ownership')
|
||||
@@ -514,4 +562,4 @@ export class LeagueController {
|
||||
): Promise<WithdrawFromLeagueWalletOutputDTO> {
|
||||
return this.leagueService.withdrawFromLeagueWallet(leagueId, input);
|
||||
}
|
||||
}
|
||||
}
|
||||
72
apps/api/src/domain/notifications/NotificationsController.ts
Normal file
72
apps/api/src/domain/notifications/NotificationsController.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Controller, Get, Post, Param, Req, Inject, HttpStatus, Res } from '@nestjs/common';
|
||||
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import type { Response } from 'express';
|
||||
|
||||
import { NotificationsService } from './NotificationsService';
|
||||
|
||||
type AuthenticatedRequest = {
|
||||
user?: { userId: string };
|
||||
};
|
||||
|
||||
@ApiTags('notifications')
|
||||
@Controller('notifications')
|
||||
export class NotificationsController {
|
||||
constructor(
|
||||
@Inject(NotificationsService) private readonly notificationsService: NotificationsService,
|
||||
) {}
|
||||
|
||||
@Get('unread')
|
||||
@ApiOperation({ summary: 'Get unread notifications for current user' })
|
||||
@ApiResponse({ status: 200, description: 'List of unread notifications' })
|
||||
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
||||
async getUnreadNotifications(
|
||||
@Req() req: AuthenticatedRequest,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
const userId = req.user?.userId;
|
||||
if (!userId) {
|
||||
res.status(HttpStatus.UNAUTHORIZED).json({ error: 'Unauthorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
const notifications = await this.notificationsService.getUnreadNotifications(userId);
|
||||
res.status(HttpStatus.OK).json({ notifications });
|
||||
}
|
||||
|
||||
@Post('read/:notificationId')
|
||||
@ApiOperation({ summary: 'Mark notification as read' })
|
||||
@ApiResponse({ status: 200, description: 'Notification marked as read' })
|
||||
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
||||
async markAsRead(
|
||||
@Param('notificationId') notificationId: string,
|
||||
@Req() req: AuthenticatedRequest,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
const userId = req.user?.userId;
|
||||
if (!userId) {
|
||||
res.status(HttpStatus.UNAUTHORIZED).json({ error: 'Unauthorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
await this.notificationsService.markAsRead(notificationId, userId);
|
||||
res.status(HttpStatus.OK).json({ success: true });
|
||||
}
|
||||
|
||||
@Get('all')
|
||||
@ApiOperation({ summary: 'Get all notifications for current user' })
|
||||
@ApiResponse({ status: 200, description: 'List of all notifications' })
|
||||
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
||||
async getAllNotifications(
|
||||
@Req() req: AuthenticatedRequest,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
const userId = req.user?.userId;
|
||||
if (!userId) {
|
||||
res.status(HttpStatus.UNAUTHORIZED).json({ error: 'Unauthorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
const notifications = await this.notificationsService.getAllNotifications(userId);
|
||||
res.status(HttpStatus.OK).json({ notifications });
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { NotificationsPersistenceModule } from '../../persistence/notifications/NotificationsPersistenceModule';
|
||||
import { NotificationsController } from './NotificationsController';
|
||||
import { NotificationsService } from './NotificationsService';
|
||||
|
||||
@Module({
|
||||
imports: [NotificationsPersistenceModule],
|
||||
exports: [NotificationsPersistenceModule],
|
||||
controllers: [NotificationsController],
|
||||
providers: [NotificationsService],
|
||||
exports: [NotificationsPersistenceModule, NotificationsService],
|
||||
})
|
||||
export class NotificationsModule {}
|
||||
35
apps/api/src/domain/notifications/NotificationsService.ts
Normal file
35
apps/api/src/domain/notifications/NotificationsService.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { INotificationRepository } from '@core/notifications/domain/repositories/INotificationRepository';
|
||||
import { NOTIFICATION_REPOSITORY_TOKEN } from '../../persistence/notifications/NotificationsPersistenceTokens';
|
||||
|
||||
@Injectable()
|
||||
export class NotificationsService {
|
||||
constructor(
|
||||
@Inject(NOTIFICATION_REPOSITORY_TOKEN)
|
||||
private readonly notificationRepository: INotificationRepository,
|
||||
) {}
|
||||
|
||||
async getUnreadNotifications(userId: string): Promise<Array<Record<string, unknown>>> {
|
||||
const notifications = await this.notificationRepository.findUnreadByRecipientId(userId);
|
||||
return notifications.map(n => n.toJSON() as Record<string, unknown>);
|
||||
}
|
||||
|
||||
async getAllNotifications(userId: string): Promise<Array<Record<string, unknown>>> {
|
||||
const notifications = await this.notificationRepository.findByRecipientId(userId);
|
||||
return notifications.map(n => n.toJSON() as Record<string, unknown>);
|
||||
}
|
||||
|
||||
async markAsRead(notificationId: string, userId: string): Promise<void> {
|
||||
const notification = await this.notificationRepository.findById(notificationId);
|
||||
if (!notification) {
|
||||
throw new Error('Notification not found');
|
||||
}
|
||||
|
||||
if (notification.recipientId !== userId) {
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
|
||||
const updated = notification.markAsRead();
|
||||
await this.notificationRepository.update(updated);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Controller, Get, Post, Patch, Body, Req, Param, Inject } from '@nestjs/common';
|
||||
import { Controller, Get, Post, Patch, Body, Req, Param, Inject, NotFoundException } from '@nestjs/common';
|
||||
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
|
||||
import { Public } from '../auth/Public';
|
||||
|
||||
@@ -37,9 +37,13 @@ export class TeamController {
|
||||
@ApiOperation({ summary: 'Get team details' })
|
||||
@ApiResponse({ status: 200, description: 'Team details', type: GetTeamDetailsOutputDTO })
|
||||
@ApiResponse({ status: 404, description: 'Team not found' })
|
||||
async getDetails(@Param('teamId') teamId: string, @Req() req: RequestWithUser): Promise<GetTeamDetailsOutputDTO | null> {
|
||||
async getDetails(@Param('teamId') teamId: string, @Req() req: RequestWithUser): Promise<GetTeamDetailsOutputDTO> {
|
||||
const userId = req.user?.userId;
|
||||
return await this.teamService.getDetails(teamId, userId);
|
||||
const result = await this.teamService.getDetails(teamId, userId);
|
||||
if (!result) {
|
||||
throw new NotFoundException('Team not found');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Public()
|
||||
@@ -90,4 +94,15 @@ export class TeamController {
|
||||
async getMembership(@Param('teamId') teamId: string, @Param('driverId') driverId: string): Promise<GetTeamMembershipOutputDTO | null> {
|
||||
return await this.teamService.getMembership(teamId, driverId);
|
||||
}
|
||||
}
|
||||
|
||||
@Post(':teamId/join')
|
||||
@ApiOperation({ summary: 'Join a team (actor derived from session)' })
|
||||
@ApiResponse({ status: 200, description: 'Joined team successfully' })
|
||||
async joinTeam(
|
||||
@Param('teamId') teamId: string,
|
||||
@Req() req: RequestWithUser,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const userId = req.user?.userId;
|
||||
return await this.teamService.joinTeam(teamId, userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import { CreateTeamUseCase, CreateTeamInput } from '@core/racing/application/use
|
||||
import { UpdateTeamUseCase, UpdateTeamInput } from '@core/racing/application/use-cases/UpdateTeamUseCase';
|
||||
import { GetDriverTeamUseCase } from '@core/racing/application/use-cases/GetDriverTeamUseCase';
|
||||
import { GetTeamMembershipUseCase } from '@core/racing/application/use-cases/GetTeamMembershipUseCase';
|
||||
import { JoinTeamUseCase } from '@core/racing/application/use-cases/JoinTeamUseCase';
|
||||
|
||||
// Tokens
|
||||
import { TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, LOGGER_TOKEN, TEAM_STATS_REPOSITORY_TOKEN } from './TeamTokens';
|
||||
@@ -307,4 +308,22 @@ export class TeamService {
|
||||
isActive: value.membership.isActive,
|
||||
} : null;
|
||||
}
|
||||
}
|
||||
|
||||
async joinTeam(teamId: string, userId?: string): Promise<Record<string, unknown>> {
|
||||
this.logger.debug(`[TeamService] Joining team ${teamId} for user ${userId}`);
|
||||
|
||||
if (!userId) {
|
||||
throw new Error('User ID is required');
|
||||
}
|
||||
|
||||
const useCase = new JoinTeamUseCase(this.teamRepository, this.membershipRepository, this.logger);
|
||||
const result = await useCase.execute({ teamId, driverId: userId });
|
||||
if (result.isErr()) {
|
||||
const error = result.error;
|
||||
this.logger.error(`Error joining team ${teamId}: ${error?.details?.message || 'Unknown error'}`);
|
||||
return { success: false, error: error?.details?.message || 'Unknown error' };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user