This commit is contained in:
2025-12-16 15:42:38 +01:00
parent 29410708c8
commit 362894d1a5
147 changed files with 780 additions and 375 deletions

View File

@@ -0,0 +1,93 @@
import { Controller, Get, Post, Patch, Body, Param } from '@nestjs/common';
import { ApiTags, ApiResponse, ApiOperation, ApiBody } from '@nestjs/swagger';
import { TeamService } from './TeamService';
import { AllTeamsViewModel, DriverTeamViewModel, TeamDetailsViewModel, TeamMembersViewModel, TeamJoinRequestsViewModel, CreateTeamInput, CreateTeamOutput, UpdateTeamInput, UpdateTeamOutput, ApproveTeamJoinRequestInput, ApproveTeamJoinRequestOutput, RejectTeamJoinRequestInput, RejectTeamJoinRequestOutput } from './dto/TeamDto';
@ApiTags('teams')
@Controller('teams')
export class TeamController {
constructor(private readonly teamService: TeamService) {}
@Get('all')
@ApiOperation({ summary: 'Get all teams' })
@ApiResponse({ status: 200, description: 'List of all teams', type: AllTeamsViewModel })
async getAllTeams(): Promise<AllTeamsViewModel> {
return this.teamService.getAllTeams();
}
@Get(':teamId')
@ApiOperation({ summary: 'Get team details' })
@ApiResponse({ status: 200, description: 'Team details', type: TeamDetailsViewModel })
@ApiResponse({ status: 404, description: 'Team not found' })
async getTeamDetails(
@Param('teamId') teamId: string,
): Promise<TeamDetailsViewModel | null> {
return this.teamService.getTeamDetails(teamId);
}
@Get(':teamId/members')
@ApiOperation({ summary: 'Get team members' })
@ApiResponse({ status: 200, description: 'Team members', type: TeamMembersViewModel })
async getTeamMembers(@Param('teamId') teamId: string): Promise<TeamMembersViewModel> {
return this.teamService.getTeamMembers(teamId);
}
@Get(':teamId/join-requests')
@ApiOperation({ summary: 'Get team join requests' })
@ApiResponse({ status: 200, description: 'Team join requests', type: TeamJoinRequestsViewModel })
async getTeamJoinRequests(@Param('teamId') teamId: string): Promise<TeamJoinRequestsViewModel> {
return this.teamService.getTeamJoinRequests(teamId);
}
@Post(':teamId/join-requests/approve')
@ApiOperation({ summary: 'Approve a team join request' })
@ApiBody({ type: ApproveTeamJoinRequestInput })
@ApiResponse({ status: 200, description: 'Join request approved', type: ApproveTeamJoinRequestOutput })
@ApiResponse({ status: 404, description: 'Join request not found' })
async approveJoinRequest(
@Param('teamId') teamId: string,
@Body() input: ApproveTeamJoinRequestInput,
): Promise<ApproveTeamJoinRequestOutput> {
return this.teamService.approveTeamJoinRequest({ ...input, teamId });
}
@Post(':teamId/join-requests/reject')
@ApiOperation({ summary: 'Reject a team join request' })
@ApiBody({ type: RejectTeamJoinRequestInput })
@ApiResponse({ status: 200, description: 'Join request rejected', type: RejectTeamJoinRequestOutput })
@ApiResponse({ status: 404, description: 'Join request not found' })
async rejectJoinRequest(
@Param('teamId') teamId: string,
@Body() input: RejectTeamJoinRequestInput,
): Promise<RejectTeamJoinRequestOutput> {
return this.teamService.rejectTeamJoinRequest({ ...input, teamId });
}
@Post()
@ApiOperation({ summary: 'Create a new team' })
@ApiBody({ type: CreateTeamInput })
@ApiResponse({ status: 201, description: 'Team created successfully', type: CreateTeamOutput })
async createTeam(@Body() input: CreateTeamInput): Promise<CreateTeamOutput> {
return this.teamService.createTeam(input);
}
@Patch(':teamId')
@ApiOperation({ summary: 'Update team details' })
@ApiBody({ type: UpdateTeamInput })
@ApiResponse({ status: 200, description: 'Team updated successfully', type: UpdateTeamOutput })
@ApiResponse({ status: 404, description: 'Team not found' })
async updateTeam(
@Param('teamId') teamId: string,
@Body() input: UpdateTeamInput,
): Promise<UpdateTeamOutput> {
return this.teamService.updateTeam({ ...input, teamId });
}
@Get('driver/:driverId')
@ApiOperation({ summary: 'Get team for a driver' })
@ApiResponse({ status: 200, description: 'Driver team membership', type: DriverTeamViewModel })
@ApiResponse({ status: 404, description: 'Driver not in a team' })
async getDriverTeam(@Param('driverId') driverId: string): Promise<DriverTeamViewModel | null> {
return this.teamService.getDriverTeam({ teamId: '', driverId });
}
}

View File

@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TeamService } from './TeamService';
import { TeamController } from './TeamController';
import { TeamProviders } from './TeamProviders';
@Module({
controllers: [TeamController],
providers: TeamProviders,
exports: [TeamService],
})
export class TeamModule {}

View File

@@ -0,0 +1,153 @@
import { Provider } from '@nestjs/common';
import { TeamService } from './TeamService';
// Import core interfaces
import { ITeamRepository } from '@core/racing/domain/repositories/ITeamRepository';
import { ITeamMembershipRepository } from '@core/racing/domain/repositories/ITeamMembershipRepository';
import { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
import { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
import type { Logger } from '@core/shared/application/Logger';
// Import concrete in-memory implementations
import { InMemoryTeamRepository } from '@adapters/racing/persistence/inmemory/InMemoryTeamRepository';
import { InMemoryTeamMembershipRepository } from '@adapters/racing/persistence/inmemory/InMemoryTeamMembershipRepository';
import { InMemoryDriverRepository } from '@adapters/racing/persistence/inmemory/InMemoryDriverRepository';
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
// Import use cases
import { GetAllTeamsUseCase } from '@core/racing/application/use-cases/GetAllTeamsUseCase';
import { GetDriverTeamUseCase } from '@core/racing/application/use-cases/GetDriverTeamUseCase';
import { GetTeamDetailsUseCase } from '@core/racing/application/use-cases/GetTeamDetailsUseCase';
import { GetTeamMembersUseCase } from '@core/racing/application/use-cases/GetTeamMembersUseCase';
import { GetTeamJoinRequestsUseCase } from '@core/racing/application/use-cases/GetTeamJoinRequestsUseCase';
import { CreateTeamUseCase } from '@core/racing/application/use-cases/CreateTeamUseCase';
import { UpdateTeamUseCase } from '@core/racing/application/use-cases/UpdateTeamUseCase';
import { ApproveTeamJoinRequestUseCase } from '@core/racing/application/use-cases/ApproveTeamJoinRequestUseCase';
import { RejectTeamJoinRequestUseCase } from '@core/racing/application/use-cases/RejectTeamJoinRequestUseCase';
// Import presenters for use case initialization
import { DriverTeamPresenter } from './presenters/DriverTeamPresenter';
import { TeamMembersPresenter } from './presenters/TeamMembersPresenter';
import { TeamJoinRequestsPresenter } from './presenters/TeamJoinRequestsPresenter';
// Tokens
export const TEAM_REPOSITORY_TOKEN = 'ITeamRepository';
export const TEAM_MEMBERSHIP_REPOSITORY_TOKEN = 'ITeamMembershipRepository';
export const DRIVER_REPOSITORY_TOKEN = 'IDriverRepository';
export const IMAGE_SERVICE_TOKEN = 'IImageServicePort';
export const TEAM_GET_ALL_USE_CASE_TOKEN = 'GetAllTeamsUseCase';
export const TEAM_GET_DRIVER_TEAM_USE_CASE_TOKEN = 'GetDriverTeamUseCase';
export const TEAM_GET_DETAILS_USE_CASE_TOKEN = 'GetTeamDetailsUseCase';
export const TEAM_GET_MEMBERS_USE_CASE_TOKEN = 'GetTeamMembersUseCase';
export const TEAM_GET_JOIN_REQUESTS_USE_CASE_TOKEN = 'GetTeamJoinRequestsUseCase';
export const TEAM_CREATE_USE_CASE_TOKEN = 'CreateTeamUseCase';
export const TEAM_UPDATE_USE_CASE_TOKEN = 'UpdateTeamUseCase';
export const TEAM_APPROVE_JOIN_REQUEST_USE_CASE_TOKEN = 'ApproveTeamJoinRequestUseCase';
export const TEAM_REJECT_JOIN_REQUEST_USE_CASE_TOKEN = 'RejectTeamJoinRequestUseCase';
export const TEAM_LOGGER_TOKEN = 'Logger';
// Simple image service implementation for team module
class SimpleImageService implements IImageServicePort {
getDriverAvatar(driverId: string): string {
return `/api/media/avatars/${driverId}`;
}
getTeamLogo(teamId: string): string {
return `/api/media/teams/${teamId}/logo`;
}
getLeagueCover(leagueId: string): string {
return `/api/media/leagues/${leagueId}/cover`;
}
getLeagueLogo(leagueId: string): string {
return `/api/media/leagues/${leagueId}/logo`;
}
}
export const TeamProviders: Provider[] = [
TeamService, // Provide the service itself
{
provide: TEAM_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryTeamRepository(logger),
inject: [TEAM_LOGGER_TOKEN],
},
{
provide: TEAM_MEMBERSHIP_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryTeamMembershipRepository(logger),
inject: [TEAM_LOGGER_TOKEN],
},
{
provide: DRIVER_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryDriverRepository(logger),
inject: [TEAM_LOGGER_TOKEN],
},
{
provide: IMAGE_SERVICE_TOKEN,
useClass: SimpleImageService,
},
{
provide: TEAM_LOGGER_TOKEN,
useClass: ConsoleLogger,
},
// Use cases
{
provide: TEAM_GET_ALL_USE_CASE_TOKEN,
useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository, logger: Logger) =>
new GetAllTeamsUseCase(teamRepo, membershipRepo, logger),
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, TEAM_LOGGER_TOKEN],
},
{
provide: TEAM_GET_DRIVER_TEAM_USE_CASE_TOKEN,
useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository, logger: Logger) =>
new GetDriverTeamUseCase(teamRepo, membershipRepo, logger, new DriverTeamPresenter()),
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, TEAM_LOGGER_TOKEN],
},
{
provide: TEAM_GET_DETAILS_USE_CASE_TOKEN,
useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository) =>
new GetTeamDetailsUseCase(teamRepo, membershipRepo),
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN],
},
{
provide: TEAM_GET_MEMBERS_USE_CASE_TOKEN,
useFactory: (
membershipRepo: ITeamMembershipRepository,
driverRepo: IDriverRepository,
imageService: IImageServicePort,
logger: Logger,
) => new GetTeamMembersUseCase(membershipRepo, driverRepo, imageService, logger, new TeamMembersPresenter()),
inject: [TEAM_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, IMAGE_SERVICE_TOKEN, TEAM_LOGGER_TOKEN],
},
{
provide: TEAM_GET_JOIN_REQUESTS_USE_CASE_TOKEN,
useFactory: (
membershipRepo: ITeamMembershipRepository,
driverRepo: IDriverRepository,
imageService: IImageServicePort,
logger: Logger,
) => new GetTeamJoinRequestsUseCase(membershipRepo, driverRepo, imageService, logger, new TeamJoinRequestsPresenter()),
inject: [TEAM_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, IMAGE_SERVICE_TOKEN, TEAM_LOGGER_TOKEN],
},
{
provide: TEAM_CREATE_USE_CASE_TOKEN,
useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository) =>
new CreateTeamUseCase(teamRepo, membershipRepo),
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN],
},
{
provide: TEAM_UPDATE_USE_CASE_TOKEN,
useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository) =>
new UpdateTeamUseCase(teamRepo, membershipRepo),
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN],
},
{
provide: TEAM_APPROVE_JOIN_REQUEST_USE_CASE_TOKEN,
useFactory: (membershipRepo: ITeamMembershipRepository, logger: Logger) =>
new ApproveTeamJoinRequestUseCase(membershipRepo, logger),
inject: [TEAM_MEMBERSHIP_REPOSITORY_TOKEN, TEAM_LOGGER_TOKEN],
},
{
provide: TEAM_REJECT_JOIN_REQUEST_USE_CASE_TOKEN,
useFactory: (membershipRepo: ITeamMembershipRepository) =>
new RejectTeamJoinRequestUseCase(membershipRepo),
inject: [TEAM_MEMBERSHIP_REPOSITORY_TOKEN],
},
];

View File

@@ -0,0 +1,168 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TeamService } from './TeamService';
import { GetAllTeamsUseCase } from '@core/racing/application/use-cases/GetAllTeamsUseCase';
import { GetDriverTeamUseCase } from '@core/racing/application/use-cases/GetDriverTeamUseCase';
import type { Logger } from '@core/shared/application/Logger';
import { AllTeamsPresenter } from './presenters/AllTeamsPresenter';
import { DriverTeamPresenter } from './presenters/DriverTeamPresenter';
import { AllTeamsViewModel, DriverTeamViewModel, GetDriverTeamQuery } from './dto/TeamDto';
import { TEAM_GET_ALL_USE_CASE_TOKEN, TEAM_GET_DRIVER_TEAM_USE_CASE_TOKEN, TEAM_LOGGER_TOKEN } from './TeamProviders';
describe('TeamService', () => {
let service: TeamService;
let getAllTeamsUseCase: jest.Mocked<GetAllTeamsUseCase>;
let getDriverTeamUseCase: jest.Mocked<GetDriverTeamUseCase>;
let logger: jest.Mocked<Logger>;
beforeEach(async () => {
const mockGetAllTeamsUseCase = {
execute: jest.fn(),
};
const mockGetDriverTeamUseCase = {
execute: jest.fn(),
};
const mockLogger = {
debug: jest.fn(),
info: jest.fn(),
error: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
TeamService,
{
provide: TEAM_GET_ALL_USE_CASE_TOKEN,
useValue: mockGetAllTeamsUseCase,
},
{
provide: TEAM_GET_DRIVER_TEAM_USE_CASE_TOKEN,
useValue: mockGetDriverTeamUseCase,
},
{
provide: TEAM_LOGGER_TOKEN,
useValue: mockLogger,
},
],
}).compile();
service = module.get<TeamService>(TeamService);
getAllTeamsUseCase = module.get(TEAM_GET_ALL_USE_CASE_TOKEN);
getDriverTeamUseCase = module.get(TEAM_GET_DRIVER_TEAM_USE_CASE_TOKEN);
logger = module.get(TEAM_LOGGER_TOKEN);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('getAllTeams', () => {
it('should create presenter, call use case, and return view model', async () => {
const mockViewModel: AllTeamsViewModel = {
teams: [],
totalCount: 0,
};
const mockPresenter = {
reset: jest.fn(),
present: jest.fn(),
get viewModel(): AllTeamsViewModel {
return mockViewModel;
},
};
// Mock the presenter constructor
const originalConstructor = AllTeamsPresenter;
(AllTeamsPresenter as any) = jest.fn().mockImplementation(() => mockPresenter);
// Mock the use case to call the presenter
getAllTeamsUseCase.execute.mockImplementation(async (input, presenter) => {
presenter.present({ teams: [] });
});
const result = await service.getAllTeams();
expect(AllTeamsPresenter).toHaveBeenCalled();
expect(getAllTeamsUseCase.execute).toHaveBeenCalledWith(undefined, mockPresenter);
expect(result).toBe(mockViewModel);
// Restore
AllTeamsPresenter = originalConstructor;
});
});
describe('getDriverTeam', () => {
it('should create presenter, call use case, and return view model', async () => {
const query: GetDriverTeamQuery = { teamId: 'team1', driverId: 'driver1' };
const mockViewModel: DriverTeamViewModel = {
team: {
id: 'team1',
name: 'Team 1',
tag: 'T1',
description: 'Description',
ownerId: 'driver1',
leagues: [],
},
membership: {
role: 'owner' as any,
joinedAt: new Date(),
isActive: true,
},
isOwner: true,
canManage: true,
};
const mockPresenter = {
reset: jest.fn(),
present: jest.fn(),
get viewModel(): DriverTeamViewModel {
return mockViewModel;
},
};
// Mock the presenter constructor
const originalConstructor = DriverTeamPresenter;
(DriverTeamPresenter as any) = jest.fn().mockImplementation(() => mockPresenter);
// Mock the use case to call the presenter
getDriverTeamUseCase.execute.mockImplementation(async (input, presenter) => {
presenter.present({
team: {
id: 'team1',
name: 'Team 1',
tag: 'T1',
description: 'Description',
ownerId: 'driver1',
leagues: [],
},
membership: {
role: 'owner',
status: 'active',
joinedAt: new Date(),
},
driverId: 'driver1',
});
});
const result = await service.getDriverTeam(query);
expect(DriverTeamPresenter).toHaveBeenCalled();
expect(getDriverTeamUseCase.execute).toHaveBeenCalledWith({ driverId: 'driver1' }, mockPresenter);
expect(result).toBe(mockViewModel);
// Restore
DriverTeamPresenter = originalConstructor;
});
it('should return null on error', async () => {
const query: GetDriverTeamQuery = { teamId: 'team1', driverId: 'driver1' };
// Mock the use case to throw an error
getDriverTeamUseCase.execute.mockRejectedValue(new Error('Team not found'));
const result = await service.getDriverTeam(query);
expect(result).toBeNull();
expect(logger.error).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,168 @@
import { Injectable, Inject } from '@nestjs/common';
import { AllTeamsViewModel, GetDriverTeamQuery, DriverTeamViewModel, TeamDetailsViewModel, TeamMembersViewModel, TeamJoinRequestsViewModel, CreateTeamInput, CreateTeamOutput, UpdateTeamInput, UpdateTeamOutput, ApproveTeamJoinRequestInput, ApproveTeamJoinRequestOutput, RejectTeamJoinRequestInput, RejectTeamJoinRequestOutput } from './dto/TeamDto';
// Use cases
import { GetAllTeamsUseCase } from '@core/racing/application/use-cases/GetAllTeamsUseCase';
import { GetDriverTeamUseCase } from '@core/racing/application/use-cases/GetDriverTeamUseCase';
import { GetTeamDetailsUseCase } from '@core/racing/application/use-cases/GetTeamDetailsUseCase';
import { GetTeamMembersUseCase } from '@core/racing/application/use-cases/GetTeamMembersUseCase';
import { GetTeamJoinRequestsUseCase } from '@core/racing/application/use-cases/GetTeamJoinRequestsUseCase';
import { CreateTeamUseCase } from '@core/racing/application/use-cases/CreateTeamUseCase';
import { UpdateTeamUseCase } from '@core/racing/application/use-cases/UpdateTeamUseCase';
import { ApproveTeamJoinRequestUseCase } from '@core/racing/application/use-cases/ApproveTeamJoinRequestUseCase';
import { RejectTeamJoinRequestUseCase } from '@core/racing/application/use-cases/RejectTeamJoinRequestUseCase';
// Presenters
import { AllTeamsPresenter } from './presenters/AllTeamsPresenter';
import { DriverTeamPresenter } from './presenters/DriverTeamPresenter';
import { TeamDetailsPresenter } from './presenters/TeamDetailsPresenter';
import { TeamMembersPresenter } from './presenters/TeamMembersPresenter';
import { TeamJoinRequestsPresenter } from './presenters/TeamJoinRequestsPresenter';
// Logger
import type { Logger } from '@core/shared/application/Logger';
// Tokens
import {
TEAM_GET_ALL_USE_CASE_TOKEN,
TEAM_GET_DRIVER_TEAM_USE_CASE_TOKEN,
TEAM_GET_DETAILS_USE_CASE_TOKEN,
TEAM_GET_MEMBERS_USE_CASE_TOKEN,
TEAM_GET_JOIN_REQUESTS_USE_CASE_TOKEN,
TEAM_CREATE_USE_CASE_TOKEN,
TEAM_UPDATE_USE_CASE_TOKEN,
TEAM_APPROVE_JOIN_REQUEST_USE_CASE_TOKEN,
TEAM_REJECT_JOIN_REQUEST_USE_CASE_TOKEN,
TEAM_LOGGER_TOKEN
} from './TeamProviders';
@Injectable()
export class TeamService {
constructor(
@Inject(TEAM_GET_ALL_USE_CASE_TOKEN) private readonly getAllTeamsUseCase: GetAllTeamsUseCase,
@Inject(TEAM_GET_DRIVER_TEAM_USE_CASE_TOKEN) private readonly getDriverTeamUseCase: GetDriverTeamUseCase,
@Inject(TEAM_GET_DETAILS_USE_CASE_TOKEN) private readonly getTeamDetailsUseCase: GetTeamDetailsUseCase,
@Inject(TEAM_GET_MEMBERS_USE_CASE_TOKEN) private readonly getTeamMembersUseCase: GetTeamMembersUseCase,
@Inject(TEAM_GET_JOIN_REQUESTS_USE_CASE_TOKEN) private readonly getTeamJoinRequestsUseCase: GetTeamJoinRequestsUseCase,
@Inject(TEAM_CREATE_USE_CASE_TOKEN) private readonly createTeamUseCase: CreateTeamUseCase,
@Inject(TEAM_UPDATE_USE_CASE_TOKEN) private readonly updateTeamUseCase: UpdateTeamUseCase,
@Inject(TEAM_APPROVE_JOIN_REQUEST_USE_CASE_TOKEN) private readonly approveTeamJoinRequestUseCase: ApproveTeamJoinRequestUseCase,
@Inject(TEAM_REJECT_JOIN_REQUEST_USE_CASE_TOKEN) private readonly rejectTeamJoinRequestUseCase: RejectTeamJoinRequestUseCase,
@Inject(TEAM_LOGGER_TOKEN) private readonly logger: Logger,
) {}
async getAllTeams(): Promise<AllTeamsViewModel> {
this.logger.debug('[TeamService] Fetching all teams.');
const presenter = new AllTeamsPresenter();
await this.getAllTeamsUseCase.execute(undefined, presenter);
return presenter.viewModel as unknown as AllTeamsViewModel;
}
async getDriverTeam(query: GetDriverTeamQuery): Promise<DriverTeamViewModel | null> {
this.logger.debug(`[TeamService] Fetching driver team for driverId: ${query.driverId}`);
const presenter = new DriverTeamPresenter();
try {
await this.getDriverTeamUseCase.execute({ driverId: query.driverId }, presenter);
return presenter.viewModel as unknown as DriverTeamViewModel;
} catch (error) {
this.logger.error(`Error fetching driver team: ${error}`);
return null;
}
}
async getTeamDetails(teamId: string): Promise<TeamDetailsViewModel | null> {
this.logger.debug(`[TeamService] Fetching team details for teamId: ${teamId}`);
const presenter = new TeamDetailsPresenter();
try {
await this.getTeamDetailsUseCase.execute({ teamId, driverId: '' }, presenter);
return presenter.viewModel as unknown as TeamDetailsViewModel;
} catch (error) {
this.logger.error(`Error fetching team details: ${error}`);
return null;
}
}
async getTeamMembers(teamId: string): Promise<TeamMembersViewModel> {
this.logger.debug(`[TeamService] Fetching team members for teamId: ${teamId}`);
const presenter = new TeamMembersPresenter();
await this.getTeamMembersUseCase.execute({ teamId }, presenter);
return presenter.viewModel as unknown as TeamMembersViewModel;
}
async getTeamJoinRequests(teamId: string): Promise<TeamJoinRequestsViewModel> {
this.logger.debug(`[TeamService] Fetching join requests for teamId: ${teamId}`);
const presenter = new TeamJoinRequestsPresenter();
await this.getTeamJoinRequestsUseCase.execute({ teamId }, presenter);
return presenter.viewModel as unknown as TeamJoinRequestsViewModel;
}
async createTeam(input: CreateTeamInput): Promise<CreateTeamOutput> {
this.logger.debug('[TeamService] Creating team', input);
try {
const result = await this.createTeamUseCase.execute({
name: input.name,
tag: input.tag,
description: input.description,
ownerId: input.ownerId,
leagues: [],
});
return {
teamId: result.team.id,
success: true,
};
} catch (error) {
this.logger.error(`Error creating team: ${error}`);
throw error;
}
}
async updateTeam(input: UpdateTeamInput & { teamId: string }): Promise<UpdateTeamOutput> {
this.logger.debug('[TeamService] Updating team', input);
try {
await this.updateTeamUseCase.execute({
teamId: input.teamId,
updates: {
name: input.name,
tag: input.tag,
description: input.description,
},
updatedBy: input.updatedBy,
});
return { success: true };
} catch (error) {
this.logger.error(`Error updating team: ${error}`);
throw error;
}
}
async approveTeamJoinRequest(input: ApproveTeamJoinRequestInput & { teamId: string }): Promise<ApproveTeamJoinRequestOutput> {
this.logger.debug('[TeamService] Approving team join request', input);
try {
await this.approveTeamJoinRequestUseCase.execute({ requestId: input.requestId });
return { success: true };
} catch (error) {
this.logger.error(`Error approving join request: ${error}`);
throw error;
}
}
async rejectTeamJoinRequest(input: RejectTeamJoinRequestInput & { teamId: string }): Promise<RejectTeamJoinRequestOutput> {
this.logger.debug('[TeamService] Rejecting team join request', input);
try {
await this.rejectTeamJoinRequestUseCase.execute({ requestId: input.requestId });
return { success: true };
} catch (error) {
this.logger.error(`Error rejecting join request: ${error}`);
throw error;
}
}
}

View File

@@ -0,0 +1,298 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNotEmpty, IsBoolean, IsOptional } from 'class-validator';
export class TeamListItemViewModel {
@ApiProperty()
id: string;
@ApiProperty()
name: string;
@ApiProperty()
tag: string;
@ApiProperty()
description: string;
@ApiProperty()
memberCount: number;
@ApiProperty({ type: [String] })
leagues: string[];
@ApiProperty({ required: false })
specialization?: 'endurance' | 'sprint' | 'mixed';
@ApiProperty({ required: false })
region?: string;
@ApiProperty({ type: [String], required: false })
languages?: string[];
}
export class AllTeamsViewModel {
@ApiProperty({ type: [TeamListItemViewModel] })
teams: TeamListItemViewModel[];
@ApiProperty()
totalCount: number;
}
export class TeamViewModel {
@ApiProperty()
id: string;
@ApiProperty()
name: string;
@ApiProperty()
tag: string;
@ApiProperty()
description: string;
@ApiProperty()
ownerId: string;
@ApiProperty({ type: [String] })
leagues: string[];
@ApiProperty({ required: false })
createdAt?: string;
@ApiProperty({ required: false })
specialization?: 'endurance' | 'sprint' | 'mixed';
@ApiProperty({ required: false })
region?: string;
@ApiProperty({ type: [String], required: false })
languages?: string[];
}
export enum MembershipRole {
OWNER = 'owner',
MANAGER = 'manager',
MEMBER = 'member',
}
export enum MembershipStatus {
ACTIVE = 'active',
PENDING = 'pending',
INVITED = 'invited',
INACTIVE = 'inactive',
}
export class MembershipViewModel {
@ApiProperty()
role: 'owner' | 'manager' | 'member';
@ApiProperty()
joinedAt: string;
@ApiProperty()
isActive: boolean;
}
export class DriverTeamViewModel {
@ApiProperty({ type: TeamViewModel })
team: TeamViewModel;
@ApiProperty({ type: MembershipViewModel })
membership: MembershipViewModel;
@ApiProperty()
isOwner: boolean;
@ApiProperty()
canManage: boolean;
}
export class GetDriverTeamQuery {
@ApiProperty()
@IsString()
teamId: string;
@ApiProperty()
@IsString()
driverId: string;
}
export class TeamDetailsViewModel {
@ApiProperty({ type: TeamViewModel })
team: TeamViewModel;
@ApiProperty({ type: MembershipViewModel, nullable: true })
membership: MembershipViewModel | null;
@ApiProperty()
canManage: boolean;
}
export class TeamMemberViewModel {
@ApiProperty()
driverId: string;
@ApiProperty()
driverName: string;
@ApiProperty()
role: 'owner' | 'manager' | 'member';
@ApiProperty()
joinedAt: string;
@ApiProperty()
isActive: boolean;
@ApiProperty()
avatarUrl: string;
}
export class TeamMembersViewModel {
@ApiProperty({ type: [TeamMemberViewModel] })
members: TeamMemberViewModel[];
@ApiProperty()
totalCount: number;
@ApiProperty()
ownerCount: number;
@ApiProperty()
managerCount: number;
@ApiProperty()
memberCount: number;
}
export class TeamJoinRequestViewModel {
@ApiProperty()
requestId: string;
@ApiProperty()
driverId: string;
@ApiProperty()
driverName: string;
@ApiProperty()
teamId: string;
@ApiProperty()
status: 'pending' | 'approved' | 'rejected';
@ApiProperty()
requestedAt: string;
@ApiProperty()
avatarUrl: string;
}
export class TeamJoinRequestsViewModel {
@ApiProperty({ type: [TeamJoinRequestViewModel] })
requests: TeamJoinRequestViewModel[];
@ApiProperty()
pendingCount: number;
@ApiProperty()
totalCount: number;
}
export class CreateTeamInput {
@ApiProperty()
@IsString()
@IsNotEmpty()
name: string;
@ApiProperty()
@IsString()
@IsNotEmpty()
tag: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
description?: string;
@ApiProperty()
@IsString()
ownerId: string;
}
export class CreateTeamOutput {
@ApiProperty()
@IsString()
teamId: string;
@ApiProperty()
@IsBoolean()
success: boolean;
}
export class UpdateTeamInput {
@ApiProperty({ required: false })
@IsOptional()
@IsString()
teamId?: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
name?: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
tag?: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
description?: string;
@ApiProperty()
@IsString()
updatedBy: string;
}
export class UpdateTeamOutput {
@ApiProperty()
@IsBoolean()
success: boolean;
}
export class ApproveTeamJoinRequestInput {
@ApiProperty()
@IsString()
requestId: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
teamId?: string;
}
export class ApproveTeamJoinRequestOutput {
@ApiProperty()
@IsBoolean()
success: boolean;
}
export class RejectTeamJoinRequestInput {
@ApiProperty()
@IsString()
requestId: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
teamId?: string;
}
export class RejectTeamJoinRequestOutput {
@ApiProperty()
@IsBoolean()
success: boolean;
}

View File

@@ -0,0 +1,34 @@
import { IAllTeamsPresenter, AllTeamsResultDTO, AllTeamsViewModel, TeamListItemViewModel } from '@core/racing/application/presenters/IAllTeamsPresenter';
export class AllTeamsPresenter implements IAllTeamsPresenter {
private result: AllTeamsViewModel | null = null;
reset() {
this.result = null;
}
present(dto: AllTeamsResultDTO) {
const teams: TeamListItemViewModel[] = dto.teams.map(team => ({
id: team.id,
name: team.name,
tag: team.tag,
description: team.description,
memberCount: team.memberCount,
leagues: team.leagues || [],
}));
this.result = {
teams,
totalCount: teams.length,
};
}
getViewModel(): AllTeamsViewModel | null {
return this.result;
}
get viewModel(): AllTeamsViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}

View File

@@ -0,0 +1,41 @@
import { IDriverTeamPresenter, DriverTeamResultDTO, DriverTeamViewModel } from '@core/racing/application/presenters/IDriverTeamPresenter';
export class DriverTeamPresenter implements IDriverTeamPresenter {
private result: DriverTeamViewModel | null = null;
reset() {
this.result = null;
}
present(dto: DriverTeamResultDTO) {
const isOwner = dto.team.ownerId === dto.driverId;
const canManage = isOwner || dto.membership.role === 'owner' || dto.membership.role === 'manager';
this.result = {
team: {
id: dto.team.id,
name: dto.team.name,
tag: dto.team.tag,
description: dto.team.description || '',
ownerId: dto.team.ownerId,
leagues: dto.team.leagues || [],
},
membership: {
role: dto.membership.role as 'owner' | 'manager' | 'member',
joinedAt: dto.membership.joinedAt.toISOString(),
isActive: dto.membership.status === 'active',
},
isOwner,
canManage,
};
}
getViewModel(): DriverTeamViewModel | null {
return this.result;
}
get viewModel(): DriverTeamViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}

View File

@@ -0,0 +1,50 @@
import {
ITeamDetailsPresenter,
TeamDetailsResultDTO,
TeamDetailsViewModel,
} from '@core/racing/application/presenters/ITeamDetailsPresenter';
export class TeamDetailsPresenter implements ITeamDetailsPresenter {
private result: TeamDetailsViewModel | null = null;
reset() {
this.result = null;
}
present(dto: TeamDetailsResultDTO) {
const { team, membership } = dto;
const canManage =
membership !== null &&
(membership.role === 'owner' || membership.role === 'manager');
this.result = {
team: {
id: team.id,
name: team.name,
tag: team.tag,
description: team.description,
ownerId: team.ownerId,
leagues: team.leagues || [],
createdAt: team.createdAt?.toISOString() || new Date().toISOString(),
},
membership: membership
? {
role: membership.role as 'owner' | 'manager' | 'member',
joinedAt: membership.joinedAt.toISOString(),
isActive: membership.status === 'active',
}
: null,
canManage,
};
}
getViewModel(): TeamDetailsViewModel | null {
return this.result;
}
get viewModel(): TeamDetailsViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}

View File

@@ -0,0 +1,43 @@
import {
ITeamJoinRequestsPresenter,
TeamJoinRequestsResultDTO,
TeamJoinRequestsViewModel,
TeamJoinRequestViewModel,
} from '@core/racing/application/presenters/ITeamJoinRequestsPresenter';
export class TeamJoinRequestsPresenter implements ITeamJoinRequestsPresenter {
private result: TeamJoinRequestsViewModel | null = null;
reset() {
this.result = null;
}
present(dto: TeamJoinRequestsResultDTO) {
const { requests, driverNames, avatarUrls } = dto;
const requestViewModels: TeamJoinRequestViewModel[] = requests.map((request) => ({
requestId: request.id,
driverId: request.driverId,
driverName: driverNames[request.driverId] || 'Unknown',
teamId: request.teamId,
status: 'pending' as const,
requestedAt: request.requestedAt.toISOString(),
avatarUrl: avatarUrls[request.driverId] || '',
}));
this.result = {
requests: requestViewModels,
pendingCount: requestViewModels.length,
totalCount: requestViewModels.length,
};
}
getViewModel(): TeamJoinRequestsViewModel | null {
return this.result;
}
get viewModel(): TeamJoinRequestsViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}

View File

@@ -0,0 +1,48 @@
import {
ITeamMembersPresenter,
TeamMembersResultDTO,
TeamMembersViewModel,
TeamMemberViewModel,
} from '@core/racing/application/presenters/ITeamMembersPresenter';
export class TeamMembersPresenter implements ITeamMembersPresenter {
private result: TeamMembersViewModel | null = null;
reset() {
this.result = null;
}
present(dto: TeamMembersResultDTO) {
const { memberships, driverNames, avatarUrls } = dto;
const members: TeamMemberViewModel[] = memberships.map((membership) => ({
driverId: membership.driverId,
driverName: driverNames[membership.driverId] || 'Unknown',
role: membership.role as 'owner' | 'manager' | 'member',
joinedAt: membership.joinedAt.toISOString(),
isActive: membership.status === 'active',
avatarUrl: avatarUrls[membership.driverId] || '',
}));
const ownerCount = members.filter((m) => m.role === 'owner').length;
const managerCount = members.filter((m) => m.role === 'manager').length;
const memberCount = members.filter((m) => m.role === 'member').length;
this.result = {
members,
totalCount: members.length,
ownerCount,
managerCount,
memberCount,
};
}
getViewModel(): TeamMembersViewModel | null {
return this.result;
}
get viewModel(): TeamMembersViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}