league service

This commit is contained in:
2025-12-16 00:57:31 +01:00
parent 3b566c973d
commit 775d41e055
130 changed files with 4077 additions and 1036 deletions

View File

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

View File

@@ -1,5 +1,54 @@
import { Provider } from '@nestjs/common';
import { TeamService } from './TeamService';
export const TeamProviders = [
TeamService,
// Import core interfaces
import { ITeamRepository } from '@gridpilot/racing/domain/repositories/ITeamRepository';
import { ITeamMembershipRepository } from '@gridpilot/racing/domain/repositories/ITeamMembershipRepository';
import { Logger } from '@gridpilot/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 { ConsoleLogger } from 'adapters/logging/ConsoleLogger';
// Import use cases
import { GetAllTeamsUseCase } from '@gridpilot/racing/application/use-cases/GetAllTeamsUseCase';
import { GetDriverTeamUseCase } from '@gridpilot/racing/application/use-cases/GetDriverTeamUseCase';
// Tokens
export const TEAM_REPOSITORY_TOKEN = 'ITeamRepository';
export const TEAM_MEMBERSHIP_REPOSITORY_TOKEN = 'ITeamMembershipRepository';
export const TEAM_GET_ALL_USE_CASE_TOKEN = 'GetAllTeamsUseCase';
export const TEAM_GET_DRIVER_TEAM_USE_CASE_TOKEN = 'GetDriverTeamUseCase';
export const TEAM_LOGGER_TOKEN = 'Logger';
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: 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),
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, TEAM_LOGGER_TOKEN],
},
];

View File

@@ -0,0 +1,168 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TeamService } from './TeamService';
import { GetAllTeamsUseCase } from '@gridpilot/racing/application/use-cases/GetAllTeamsUseCase';
import { GetDriverTeamUseCase } from '@gridpilot/racing/application/use-cases/GetDriverTeamUseCase';
import { Logger } from '@gridpilot/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

@@ -1,43 +1,46 @@
import { Injectable } from '@nestjs/common';
import { AllTeamsViewModel, GetDriverTeamQuery, DriverTeamViewModel, TeamDto, MembershipDto, TeamLeagueDto, MembershipRole } from './dto/TeamDto';
import { Injectable, Inject } from '@nestjs/common';
import { AllTeamsViewModel, GetDriverTeamQuery, DriverTeamViewModel } from './dto/TeamDto';
// Use cases
import { GetAllTeamsUseCase } from '@gridpilot/racing/application/use-cases/GetAllTeamsUseCase';
import { GetDriverTeamUseCase } from '@gridpilot/racing/application/use-cases/GetDriverTeamUseCase';
// Presenters
import { AllTeamsPresenter } from './presenters/AllTeamsPresenter';
import { DriverTeamPresenter } from './presenters/DriverTeamPresenter';
// Logger
import { Logger } from '@gridpilot/shared/application/Logger';
// Tokens
import { TEAM_GET_ALL_USE_CASE_TOKEN, TEAM_GET_DRIVER_TEAM_USE_CASE_TOKEN, TEAM_LOGGER_TOKEN } from './TeamProviders';
@Injectable()
export class TeamService {
getAllTeams(): Promise<AllTeamsViewModel> {
// TODO: Implement actual logic to fetch all teams
return Promise.resolve({
teams: [],
totalCount: 0,
});
}
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_LOGGER_TOKEN) private readonly logger: Logger,
) {}
private teams: Map<string, TeamDto> = new Map(); // In-memory store for teams
async getAllTeams(): Promise<AllTeamsViewModel> {
this.logger.debug('[TeamService] Fetching all teams.');
const presenter = new AllTeamsPresenter();
await this.getAllTeamsUseCase.execute(undefined, presenter);
return presenter.viewModel;
}
async getDriverTeam(query: GetDriverTeamQuery): Promise<DriverTeamViewModel | null> {
const { teamId, driverId } = query;
this.logger.debug(`[TeamService] Fetching driver team for driverId: ${query.driverId}`);
const team = this.teams.get(teamId);
if (!team) {
const presenter = new DriverTeamPresenter();
try {
await this.getDriverTeamUseCase.execute({ driverId: query.driverId }, presenter);
return presenter.viewModel;
} catch (error) {
this.logger.error(`Error fetching driver team: ${error}`);
return null;
}
// Mock membership and roles
const membership: MembershipDto = {
role: driverId === team.ownerId ? MembershipRole.OWNER : MembershipRole.MEMBER,
joinedAt: new Date(Date.now() - 86400000 * 30), // Joined 30 days ago
isActive: true, // Always active for mock
};
const isOwner = team.ownerId === driverId;
const canManage = isOwner || membership.role === MembershipRole.MANAGER;
return {
team: team,
membership,
isOwner,
canManage,
};
}
// Add other methods related to Team logic here based on other presenters
}

View File

@@ -1,4 +1,5 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNotEmpty, IsEnum, IsBoolean, IsDate, IsOptional } from 'class-validator';
export class TeamLeagueDto {
@ApiProperty()
@@ -37,7 +38,7 @@ export class AllTeamsViewModel {
@ApiProperty()
totalCount: number;
import { IsString, IsNotEmpty, IsEnum, IsBoolean, IsDate } from 'class-validator';
}
export class TeamDto {
@ApiProperty()

View File

@@ -0,0 +1,31 @@
import { IAllTeamsPresenter, AllTeamsResultDTO, AllTeamsViewModel } from '@gridpilot/racing/application/presenters/IAllTeamsPresenter';
import { TeamListItemViewModel } from '../dto/TeamDto';
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,
};
}
get viewModel(): AllTeamsViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}

View File

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