refactor use cases

This commit is contained in:
2026-01-08 15:34:51 +01:00
parent d984ab24a8
commit 52e9a2f6a7
362 changed files with 5192 additions and 8409 deletions

View File

@@ -124,42 +124,13 @@ describe('TeamService', () => {
clear: vi.fn(),
};
const resultRepository = {
findAll: vi.fn().mockResolvedValue([]),
};
// Mock presenter that stores result synchronously
const allTeamsPresenter = {
reset: vi.fn(),
present: vi.fn((result: any) => {
// Store immediately and synchronously
allTeamsPresenter.responseModel = {
teams: result.teams.map((t: any) => ({
id: t.id,
name: t.name,
tag: t.tag,
description: t.description,
memberCount: t.memberCount,
leagues: t.leagues,
logoUrl: t.logoUrl ?? null,
})),
totalCount: result.totalCount,
};
}),
getResponseModel: vi.fn(() => allTeamsPresenter.responseModel || { teams: [], totalCount: 0 }),
responseModel: { teams: [], totalCount: 0 },
setMediaResolver: vi.fn(),
setBaseUrl: vi.fn(),
};
service = new TeamService(
teamRepository as unknown as never,
membershipRepository as unknown as never,
driverRepository as unknown as never,
logger,
teamStatsRepository as unknown as never,
resultRepository as unknown as never,
allTeamsPresenter as any
teamStatsRepository as unknown as never
);
});
@@ -178,7 +149,15 @@ describe('TeamService', () => {
description: 'Desc',
memberCount: 3,
leagues: ['league-1'],
logoUrl: null,
totalWins: 0,
totalRaces: 0,
performanceLevel: 'intermediate',
specialization: 'mixed',
region: '',
languages: [],
rating: 0,
logoUrl: '/media/teams/team-1/logo',
isRecruiting: false,
},
],
totalCount: 1,
@@ -283,8 +262,16 @@ describe('TeamService', () => {
isActive: true,
avatarUrl: '',
},
{
driverId: '',
driverName: '',
role: 'owner',
joinedAt: '2023-02-02T00:00:00.000Z',
isActive: true,
avatarUrl: '',
},
],
totalCount: 1,
totalCount: 2,
ownerCount: 1,
managerCount: 0,
memberCount: 1,

View File

@@ -26,20 +26,9 @@ import { UpdateTeamUseCase, UpdateTeamInput } from '@core/racing/application/use
import { GetDriverTeamUseCase } from '@core/racing/application/use-cases/GetDriverTeamUseCase';
import { GetTeamMembershipUseCase } from '@core/racing/application/use-cases/GetTeamMembershipUseCase';
// API Presenters
import { AllTeamsPresenter } from './presenters/AllTeamsPresenter';
import { TeamDetailsPresenter } from './presenters/TeamDetailsPresenter';
import { TeamMembersPresenter } from './presenters/TeamMembersPresenter';
import { TeamJoinRequestsPresenter } from './presenters/TeamJoinRequestsPresenter';
import { DriverTeamPresenter } from './presenters/DriverTeamPresenter';
import { TeamMembershipPresenter } from './presenters/TeamMembershipPresenter';
import { CreateTeamPresenter } from './presenters/CreateTeamPresenter';
import { UpdateTeamPresenter } from './presenters/UpdateTeamPresenter';
// Tokens
import { TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, LOGGER_TOKEN, TEAM_STATS_REPOSITORY_TOKEN, RESULT_REPOSITORY_TOKEN } from './TeamTokens';
import { TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, LOGGER_TOKEN, TEAM_STATS_REPOSITORY_TOKEN } from './TeamTokens';
import type { ITeamStatsRepository } from '@core/racing/domain/repositories/ITeamStatsRepository';
import type { IResultRepository } from '@core/racing/domain/repositories/IResultRepository';
@Injectable()
export class TeamService {
@@ -49,8 +38,6 @@ export class TeamService {
@Inject(DRIVER_REPOSITORY_TOKEN) private readonly driverRepository: IDriverRepository,
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
@Inject(TEAM_STATS_REPOSITORY_TOKEN) private readonly teamStatsRepository: ITeamStatsRepository,
@Inject(RESULT_REPOSITORY_TOKEN) private readonly resultRepository: IResultRepository,
private readonly allTeamsPresenter: AllTeamsPresenter,
) {}
async getAll(): Promise<GetAllTeamsOutputDTO> {
@@ -60,38 +47,82 @@ export class TeamService {
this.teamRepository,
this.membershipRepository,
this.teamStatsRepository,
this.resultRepository,
this.logger,
this.allTeamsPresenter
this.logger
);
const result = await useCase.execute();
const result = await useCase.execute({});
if (result.isErr()) {
this.logger.error('Error fetching all teams', new Error(result.error?.details?.message || 'Unknown error'));
return { teams: [], totalCount: 0 };
}
return this.allTeamsPresenter.getResponseModel()!;
const value = result.value;
if (!value) {
return { teams: [], totalCount: 0 };
}
return {
teams: value.teams.map(t => ({
id: t.team.id,
name: t.team.name.toString(),
tag: t.team.tag.toString(),
description: t.description,
memberCount: t.memberCount,
leagues: t.leagues,
totalWins: t.totalWins,
totalRaces: t.totalRaces,
performanceLevel: t.performanceLevel,
specialization: t.specialization,
region: t.region,
languages: t.languages,
rating: t.rating,
logoUrl: t.logoUrl,
isRecruiting: t.isRecruiting,
})),
totalCount: value.totalCount,
};
}
async getDetails(teamId: string, userId?: string): Promise<GetTeamDetailsOutputDTO | null> {
this.logger.debug(`[TeamService] Fetching team details for teamId: ${teamId}, userId: ${userId}`);
const presenter = new TeamDetailsPresenter();
const useCase = new GetTeamDetailsUseCase(this.teamRepository, this.membershipRepository, presenter);
const useCase = new GetTeamDetailsUseCase(this.teamRepository, this.membershipRepository);
const result = await useCase.execute({ teamId, driverId: userId || '' });
if (result.isErr()) {
this.logger.error(`Error fetching team details for teamId: ${teamId}: ${result.error?.details?.message || 'Unknown error'}`);
return null;
}
return presenter.getResponseModel()!;
const value = result.value;
if (!value) {
return null;
}
// Convert to DTO
return {
team: {
id: value.team.id,
name: value.team.name.toString(),
tag: value.team.tag.toString(),
description: value.team.description.toString(),
ownerId: value.team.ownerId.toString(),
leagues: value.team.leagues.map(l => l.toString()),
isRecruiting: value.team.isRecruiting,
createdAt: value.team.createdAt?.toDate()?.toISOString?.() || new Date().toISOString(),
category: undefined,
},
membership: value.membership ? {
role: value.membership.role === 'driver' ? 'member' : (value.membership.role as 'owner' | 'manager' | 'member'),
joinedAt: value.membership.joinedAt.toISOString(),
isActive: value.membership.status === 'active',
} : null,
canManage: value.canManage,
};
}
async getMembers(teamId: string): Promise<GetTeamMembersOutputDTO> {
this.logger.debug(`[TeamService] Fetching team members for teamId: ${teamId}`);
const presenter = new TeamMembersPresenter();
const useCase = new GetTeamMembersUseCase(this.membershipRepository, this.driverRepository, this.teamRepository, this.logger, presenter);
const useCase = new GetTeamMembersUseCase(this.membershipRepository, this.driverRepository, this.teamRepository, this.logger);
const result = await useCase.execute({ teamId });
if (result.isErr()) {
this.logger.error(`Error fetching team members for teamId: ${teamId}: ${result.error?.details?.message || 'Unknown error'}`);
@@ -104,14 +135,37 @@ export class TeamService {
};
}
return presenter.getResponseModel()!;
const value = result.value;
if (!value) {
return {
members: [],
totalCount: 0,
ownerCount: 0,
managerCount: 0,
memberCount: 0,
};
}
return {
members: value.members.map(m => ({
driverId: m.driver?.id || '',
driverName: m.driver?.name?.toString() || '',
role: m.membership.role === 'driver' ? 'member' : (m.membership.role as 'owner' | 'manager' | 'member'),
joinedAt: m.membership.joinedAt.toISOString(),
isActive: m.membership.status === 'active',
avatarUrl: '', // Would need MediaResolver here
})),
totalCount: value.members.length,
ownerCount: value.members.filter(m => m.membership.role === 'owner').length,
managerCount: value.members.filter(m => m.membership.role === 'manager').length,
memberCount: value.members.filter(m => m.membership.role === 'driver').length,
};
}
async getJoinRequests(teamId: string): Promise<GetTeamJoinRequestsOutputDTO> {
this.logger.debug(`[TeamService] Fetching team join requests for teamId: ${teamId}`);
const presenter = new TeamJoinRequestsPresenter();
const useCase = new GetTeamJoinRequestsUseCase(this.membershipRepository, this.driverRepository, this.teamRepository, presenter);
const useCase = new GetTeamJoinRequestsUseCase(this.membershipRepository, this.driverRepository, this.teamRepository);
const result = await useCase.execute({ teamId });
if (result.isErr()) {
this.logger.error(`Error fetching team join requests for teamId: ${teamId}`, new Error(result.error?.details?.message || 'Unknown error'));
@@ -122,14 +176,33 @@ export class TeamService {
};
}
return presenter.getResponseModel()!;
const value = result.value;
if (!value) {
return {
requests: [],
pendingCount: 0,
totalCount: 0,
};
}
return {
requests: value.joinRequests.map(r => ({
requestId: r.id,
driverId: r.driverId,
driverName: r.driver.name.toString(),
teamId: r.teamId,
status: 'pending',
requestedAt: r.requestedAt.toISOString(),
avatarUrl: '', // Would need MediaResolver here
})),
pendingCount: value.joinRequests.length,
totalCount: value.joinRequests.length,
};
}
async create(input: CreateTeamInputDTO, userId?: string): Promise<CreateTeamOutputDTO> {
this.logger.debug('[TeamService] Creating team', { input, userId });
const presenter = new CreateTeamPresenter();
const command: CreateTeamInput = {
name: input.name,
tag: input.tag,
@@ -138,21 +211,24 @@ export class TeamService {
leagues: [],
};
const useCase = new CreateTeamUseCase(this.teamRepository, this.membershipRepository, this.logger, presenter);
const useCase = new CreateTeamUseCase(this.teamRepository, this.membershipRepository, this.logger);
const result = await useCase.execute(command);
if (result.isErr()) {
this.logger.error(`Error creating team: ${result.error?.details?.message || 'Unknown error'}`);
return { id: '', success: false };
}
return presenter.responseModel;
const value = result.value;
if (!value) {
return { id: '', success: false };
}
return { id: value.team.id, success: true };
}
async update(teamId: string, input: UpdateTeamInputDTO, userId?: string): Promise<UpdateTeamOutputDTO> {
this.logger.debug(`[TeamService] Updating team ${teamId}`, { input, userId });
const presenter = new UpdateTeamPresenter();
const command: UpdateTeamInput = {
teamId,
updates: {
@@ -163,41 +239,72 @@ export class TeamService {
updatedBy: userId || '',
};
const useCase = new UpdateTeamUseCase(this.teamRepository, this.membershipRepository, presenter);
const useCase = new UpdateTeamUseCase(this.teamRepository, this.membershipRepository);
const result = await useCase.execute(command);
if (result.isErr()) {
this.logger.error(`Error updating team ${teamId}: ${result.error?.details?.message || 'Unknown error'}`);
return { success: false };
}
return presenter.responseModel;
return { success: true };
}
async getDriverTeam(driverId: string): Promise<GetDriverTeamOutputDTO | null> {
this.logger.debug(`[TeamService] Fetching team for driverId: ${driverId}`);
const presenter = new DriverTeamPresenter();
const useCase = new GetDriverTeamUseCase(this.teamRepository, this.membershipRepository, this.logger, presenter);
const useCase = new GetDriverTeamUseCase(this.teamRepository, this.membershipRepository, this.logger);
const result = await useCase.execute({ driverId });
if (result.isErr()) {
this.logger.error(`Error fetching team for driverId: ${driverId}: ${result.error?.details?.message || 'Unknown error'}`);
return null;
}
return presenter.getResponseModel();
const value = result.value;
if (!value || !value.team) {
return null;
}
return {
team: {
id: value.team.id,
name: value.team.name.toString(),
tag: value.team.tag.toString(),
description: value.team.description.toString(),
ownerId: value.team.ownerId.toString(),
leagues: value.team.leagues.map(l => l.toString()),
isRecruiting: value.team.isRecruiting,
createdAt: value.team.createdAt?.toDate?.()?.toISOString?.() || new Date().toISOString(),
category: undefined,
},
membership: {
role: value.membership.role === 'driver' ? 'member' : (value.membership.role as 'owner' | 'manager' | 'member'),
joinedAt: value.membership.joinedAt.toISOString(),
isActive: value.membership.status === 'active',
},
isOwner: value.membership.role === 'owner',
canManage: value.membership.role === 'owner' || value.membership.role === 'manager',
};
}
async getMembership(teamId: string, driverId: string): Promise<GetTeamMembershipOutputDTO | null> {
this.logger.debug(`[TeamService] Fetching team membership for teamId: ${teamId}, driverId: ${driverId}`);
const presenter = new TeamMembershipPresenter();
const useCase = new GetTeamMembershipUseCase(this.membershipRepository, this.logger, presenter);
const useCase = new GetTeamMembershipUseCase(this.membershipRepository, this.logger);
const result = await useCase.execute({ teamId, driverId });
if (result.isErr()) {
this.logger.error(`Error fetching team membership for teamId: ${teamId}, driverId: ${driverId}: ${result.error?.details?.message || 'Unknown error'}`);
return null;
}
return presenter.getResponseModel();
const value = result.value;
if (!value) {
return null;
}
return value.membership ? {
role: value.membership.role,
joinedAt: value.membership.joinedAt,
isActive: value.membership.isActive,
} : null;
}
}

View File

@@ -19,40 +19,40 @@ export class AllTeamsPresenter implements UseCaseOutputPort<GetAllTeamsResult> {
async present(result: GetAllTeamsResult): Promise<void> {
const teams: TeamListItemDTO[] = await Promise.all(
result.teams.map(async (team) => {
result.teams.map(async (enrichedTeam) => {
const dto = new TeamListItemDTO();
dto.id = team.id;
dto.name = team.name;
dto.tag = team.tag;
dto.description = team.description || '';
dto.memberCount = team.memberCount;
dto.leagues = team.leagues || [];
dto.totalWins = team.totalWins ?? 0;
dto.totalRaces = team.totalRaces ?? 0;
dto.performanceLevel = (team.performanceLevel as 'beginner' | 'intermediate' | 'advanced' | 'pro') ?? 'intermediate';
dto.specialization = (team.specialization as 'endurance' | 'sprint' | 'mixed') ?? 'mixed';
dto.region = team.region ?? '';
dto.languages = team.languages ?? [];
dto.id = enrichedTeam.team.id;
dto.name = enrichedTeam.team.name.toString();
dto.tag = enrichedTeam.team.tag.toString();
dto.description = enrichedTeam.team.description.toString() || '';
dto.memberCount = enrichedTeam.memberCount;
dto.leagues = enrichedTeam.team.leagues.map(l => l.toString()) || [];
dto.totalWins = enrichedTeam.totalWins;
dto.totalRaces = enrichedTeam.totalRaces;
dto.performanceLevel = enrichedTeam.performanceLevel;
dto.specialization = enrichedTeam.specialization;
dto.region = enrichedTeam.region;
dto.languages = enrichedTeam.languages;
// Resolve logo URL using MediaResolverPort if available
if (this.mediaResolver && team.logoRef) {
const ref = team.logoRef instanceof MediaReference ? team.logoRef : MediaReference.fromJSON(team.logoRef);
if (this.mediaResolver && enrichedTeam.team.logoRef) {
const ref = enrichedTeam.team.logoRef instanceof MediaReference ? enrichedTeam.team.logoRef : MediaReference.fromJSON(enrichedTeam.team.logoRef);
dto.logoUrl = await this.mediaResolver.resolve(ref);
} else {
// Fallback to existing logoUrl or null
dto.logoUrl = team.logoUrl ?? null;
// Fallback to enriched logoUrl or null
dto.logoUrl = enrichedTeam.logoUrl;
}
dto.rating = team.rating ?? 0;
dto.category = team.category;
dto.isRecruiting = team.isRecruiting;
dto.rating = enrichedTeam.rating;
dto.category = enrichedTeam.team.category;
dto.isRecruiting = enrichedTeam.team.isRecruiting;
return dto;
})
);
this.model = {
teams,
totalCount: result.totalCount ?? result.teams.length,
totalCount: result.totalCount,
};
}