seed data

This commit is contained in:
2025-12-30 18:33:15 +01:00
parent 83371ea839
commit 92226800df
306 changed files with 1753 additions and 501 deletions

View File

@@ -2710,6 +2710,21 @@
},
"joinedAt": {
"type": "string"
},
"rating": {
"type": "number"
},
"experienceLevel": {
"type": "string"
},
"wins": {
"type": "number"
},
"podiums": {
"type": "number"
},
"totalRaces": {
"type": "number"
}
},
"required": [
@@ -6607,6 +6622,21 @@
"items": {
"type": "string"
}
},
"totalWins": {
"type": "number"
},
"totalRaces": {
"type": "number"
},
"performanceLevel": {
"type": "string"
},
"logoUrl": {
"type": "string"
},
"rating": {
"type": "number"
}
},
"required": [

View File

@@ -18,6 +18,11 @@ export class DashboardDriverSummaryDTO {
@IsString()
avatarUrl!: string;
@ApiProperty({ nullable: true })
@IsOptional()
@IsString()
category?: string | null;
@ApiProperty({ nullable: true })
@IsOptional()
@IsNumber()

View File

@@ -20,6 +20,11 @@ export class DashboardDriverSummaryDTO {
@IsString()
avatarUrl?: string | null;
@ApiProperty({ nullable: true })
@IsOptional()
@IsString()
category?: string | null;
@ApiProperty({ nullable: true })
@IsOptional()
@IsNumber()

View File

@@ -23,6 +23,7 @@ export class DashboardOverviewPresenter implements UseCaseOutputPort<DashboardOv
name: String(data.currentDriver.driver.name),
country: String(data.currentDriver.driver.country),
avatarUrl: data.currentDriver.avatarUrl,
category: data.currentDriver.driver.category ?? null,
rating: data.currentDriver.rating,
globalRank: data.currentDriver.globalRank,
totalRaces: data.currentDriver.totalRaces,

View File

@@ -18,4 +18,7 @@ export class DriverDTO {
@ApiProperty()
joinedAt!: string;
@ApiProperty({ required: false })
category?: string;
}

View File

@@ -13,6 +13,9 @@ export class DriverLeaderboardItemDTO {
@ApiProperty()
skillLevel!: string; // Assuming skillLevel is a string like 'Rookie', 'Pro', etc.
@ApiProperty({ required: false })
category?: string;
@ApiProperty()
nationality!: string;

View File

@@ -19,6 +19,9 @@ export class DriverProfileDriverSummaryDTO {
@ApiProperty()
joinedAt!: string;
@ApiProperty({ nullable: true })
category!: string | null;
@ApiProperty({ nullable: true })
rating!: number | null;

View File

@@ -19,6 +19,9 @@ export class GetDriverOutputDTO {
@ApiProperty()
joinedAt!: string;
@ApiProperty({ required: false })
category?: string;
@ApiProperty({ required: false })
rating?: number;

View File

@@ -33,6 +33,7 @@ export class DriverPresenter {
country: driver.country.toString(),
joinedAt: driver.joinedAt.toDate().toISOString(),
...(driver.bio ? { bio: driver.bio.toString() } : {}),
...(driver.category ? { category: driver.category } : {}),
// Add stats fields
...(stats ? {
rating: stats.rating,

View File

@@ -18,6 +18,7 @@ export class DriverProfilePresenter
avatarUrl: this.getAvatarUrl(result.driverInfo.driver.id) || '',
iracingId: result.driverInfo.driver.iracingId.toString(),
joinedAt: result.driverInfo.driver.joinedAt.toDate().toISOString(),
category: result.driverInfo.driver.category || null,
rating: result.driverInfo.rating,
globalRank: result.driverInfo.globalRank,
consistency: result.driverInfo.consistency,

View File

@@ -13,6 +13,7 @@ export class DriversLeaderboardPresenter {
name: item.driver.name.toString(),
rating: item.rating,
skillLevel: item.skillLevel,
...(item.driver.category !== undefined ? { category: item.driver.category } : {}),
nationality: item.driver.country.toString(),
racesCompleted: item.racesCompleted,
wins: item.wins,

View File

@@ -95,6 +95,11 @@ export class LeagueWithCapacityAndScoringDTO {
@IsNumber()
usedSlots!: number;
@ApiProperty({ required: false, nullable: true })
@IsOptional()
@IsString()
category?: string;
@ApiProperty({ required: false, nullable: true, type: LeagueCapacityAndScoringSocialLinksDTO })
@IsOptional()
@ValidateNested()

View File

@@ -32,6 +32,7 @@ export class AllLeaguesWithCapacityAndScoringPresenter
: {}),
},
usedSlots: summary.currentDrivers,
...(summary.league.category ? { category: summary.league.category } : {}),
...mapSocialLinks(summary.league.socialLinks),
...(summary.scoringConfig && summary.game && summary.preset
? {

View File

@@ -91,6 +91,70 @@ function buildLeagueCoverSvg(leagueId: string): string {
</svg>`;
}
function buildDriverAvatarSvg(driverId: string): string {
const hue = hashToHue(driverId);
const initials = deriveLeagueLabel(driverId);
const bg = `hsl(${hue} 70% 38%)`;
const border = `hsl(${hue} 70% 28%)`;
return `<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96" role="img" aria-label="Driver avatar">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="${bg}"/>
<stop offset="100%" stop-color="hsl(${hue} 80% 46%)"/>
</linearGradient>
</defs>
<circle cx="48" cy="48" r="44" fill="url(#g)" stroke="${border}" stroke-width="3"/>
<text x="48" y="56" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial" font-size="32" font-weight="800" text-anchor="middle" fill="white">${initials}</text>
</svg>`;
}
function buildTrackImageSvg(trackId: string): string {
const hue = hashToHue(trackId);
const label = escapeXml(deriveLeagueLabel(trackId));
const bg1 = `hsl(${hue} 70% 28%)`;
const bg2 = `hsl(${(hue + 20) % 360} 65% 35%)`;
return `<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="400" viewBox="0 0 1200 400" role="img" aria-label="Track image">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="${bg1}"/>
<stop offset="100%" stop-color="${bg2}"/>
</linearGradient>
</defs>
<rect width="1200" height="400" fill="url(#bg)"/>
<!-- Track outline -->
<path d="M 200 200 Q 400 100 600 200 T 1000 200" fill="none" stroke="rgba(255,255,255,0.2)" stroke-width="8" stroke-linecap="round"/>
<path d="M 200 220 Q 400 120 600 220 T 1000 220" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="6" stroke-linecap="round"/>
<text x="64" y="110" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial" font-size="40" font-weight="800" fill="rgba(255,255,255,0.92)">Track ${label}</text>
<text x="64" y="165" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial" font-size="22" font-weight="600" fill="rgba(255,255,255,0.75)">${escapeXml(trackId)}</text>
</svg>`;
}
function buildCategoryIconSvg(categoryId: string): string {
const hue = hashToHue(categoryId);
const label = escapeXml(categoryId.substring(0, 3).toUpperCase());
const bg = `hsl(${hue} 70% 38%)`;
const border = `hsl(${hue} 70% 28%)`;
return `<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64" role="img" aria-label="Category icon">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="${bg}"/>
<stop offset="100%" stop-color="hsl(${hue} 80% 46%)"/>
</linearGradient>
</defs>
<rect x="2" y="2" width="60" height="60" rx="12" fill="url(#g)" stroke="${border}" stroke-width="2"/>
<text x="32" y="40" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial" font-size="22" font-weight="800" text-anchor="middle" fill="white">${label}</text>
</svg>`;
}
@ApiTags('media')
@Controller('media')
export class MediaController {
@@ -159,6 +223,132 @@ export class MediaController {
res.status(HttpStatus.OK).send(svg);
}
@Public()
@Get('teams/:teamId/logo')
@ApiOperation({ summary: 'Get team logo (placeholder)' })
@ApiParam({ name: 'teamId', description: 'Team ID' })
async getTeamLogo(
@Param('teamId') teamId: string,
@Res() res: Response,
): Promise<void> {
const svg = buildLeagueLogoSvg(teamId);
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
res.setHeader('Cache-Control', 'public, max-age=86400');
res.status(HttpStatus.OK).send(svg);
}
@Public()
@Get('team/:teamId/logo')
@ApiOperation({ summary: 'Get team logo (singular path)' })
@ApiParam({ name: 'teamId', description: 'Team ID' })
async getTeamLogoSingular(
@Param('teamId') teamId: string,
@Res() res: Response,
): Promise<void> {
const svg = buildLeagueLogoSvg(teamId);
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
res.setHeader('Cache-Control', 'public, max-age=86400');
res.status(HttpStatus.OK).send(svg);
}
@Public()
@Get('team/:teamId/logo.png')
@ApiOperation({ summary: 'Get team logo with .png extension' })
@ApiParam({ name: 'teamId', description: 'Team ID' })
async getTeamLogoPng(
@Param('teamId') teamId: string,
@Res() res: Response,
): Promise<void> {
const svg = buildLeagueLogoSvg(teamId);
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
res.setHeader('Cache-Control', 'public, max-age=86400');
res.status(HttpStatus.OK).send(svg);
}
@Public()
@Get('teams/:teamId/cover')
@ApiOperation({ summary: 'Get team cover (placeholder)' })
@ApiParam({ name: 'teamId', description: 'Team ID' })
async getTeamCover(
@Param('teamId') teamId: string,
@Res() res: Response,
): Promise<void> {
const svg = buildLeagueCoverSvg(teamId);
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
res.setHeader('Cache-Control', 'public, max-age=86400');
res.status(HttpStatus.OK).send(svg);
}
@Public()
@Get('drivers/:driverId/avatar')
@ApiOperation({ summary: 'Get driver avatar (placeholder)' })
@ApiParam({ name: 'driverId', description: 'Driver ID' })
async getDriverAvatar(
@Param('driverId') driverId: string,
@Res() res: Response,
): Promise<void> {
const svg = buildDriverAvatarSvg(driverId);
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
res.setHeader('Cache-Control', 'public, max-age=86400');
res.status(HttpStatus.OK).send(svg);
}
@Public()
@Get('avatar/:driverId')
@ApiOperation({ summary: 'Get driver avatar (alternative path)' })
@ApiParam({ name: 'driverId', description: 'Driver ID' })
async getDriverAvatarAlt(
@Param('driverId') driverId: string,
@Res() res: Response,
): Promise<void> {
const svg = buildDriverAvatarSvg(driverId);
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
res.setHeader('Cache-Control', 'public, max-age=86400');
res.status(HttpStatus.OK).send(svg);
}
@Public()
@Get('tracks/:trackId/image')
@ApiOperation({ summary: 'Get track image (placeholder)' })
@ApiParam({ name: 'trackId', description: 'Track ID' })
async getTrackImage(
@Param('trackId') trackId: string,
@Res() res: Response,
): Promise<void> {
const svg = buildTrackImageSvg(trackId);
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
res.setHeader('Cache-Control', 'public, max-age=86400');
res.status(HttpStatus.OK).send(svg);
}
@Public()
@Get('categories/:categoryId/icon')
@ApiOperation({ summary: 'Get category icon (placeholder)' })
@ApiParam({ name: 'categoryId', description: 'Category ID' })
async getCategoryIcon(
@Param('categoryId') categoryId: string,
@Res() res: Response,
): Promise<void> {
const svg = buildCategoryIconSvg(categoryId);
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
res.setHeader('Cache-Control', 'public, max-age=86400');
res.status(HttpStatus.OK).send(svg);
}
@Public()
@Get('sponsors/:sponsorId/logo')
@ApiOperation({ summary: 'Get sponsor logo (placeholder)' })
@ApiParam({ name: 'sponsorId', description: 'Sponsor ID' })
async getSponsorLogo(
@Param('sponsorId') sponsorId: string,
@Res() res: Response,
): Promise<void> {
const svg = buildLeagueLogoSvg(sponsorId);
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
res.setHeader('Cache-Control', 'public, max-age=86400');
res.status(HttpStatus.OK).send(svg);
}
@Public()
@Get(':mediaId')
@ApiOperation({ summary: 'Get media by ID' })
@@ -237,4 +427,4 @@ export class MediaController {
res.status(HttpStatus.BAD_REQUEST).json(dto);
}
}
}
}

View File

@@ -58,7 +58,7 @@ describe('TeamController', () => {
it('should return team details', async () => {
const teamId = 'team-123';
const userId = 'user-456';
const result = { team: { id: teamId, name: 'Team', tag: 'TAG', description: 'Desc', ownerId: 'owner', leagues: [] }, membership: null, canManage: false };
const result = { team: { id: teamId, name: 'Team', tag: 'TAG', description: 'Desc', ownerId: 'owner', leagues: [], isRecruiting: false }, membership: null, canManage: false };
service.getDetails.mockResolvedValue(result);
const mockReq = { user: { userId } } as any;
@@ -132,7 +132,7 @@ describe('TeamController', () => {
describe('getDriverTeam', () => {
it('should return driver team', async () => {
const driverId = 'driver-123';
const result = { team: { id: 'team-456', name: 'Team', tag: 'TAG', description: 'Desc', ownerId: 'owner', leagues: [] }, membership: { role: 'member' as const, joinedAt: '2023-01-01', isActive: true }, isOwner: false, canManage: false };
const result = { team: { id: 'team-456', name: 'Team', tag: 'TAG', description: 'Desc', ownerId: 'owner', leagues: [], isRecruiting: false }, membership: { role: 'member' as const, joinedAt: '2023-01-01', isActive: true }, isOwner: false, canManage: false };
service.getDriverTeam.mockResolvedValue(result);
const response = await controller.getDriverTeam(driverId);

View File

@@ -1,6 +1,6 @@
import { Provider } from '@nestjs/common';
import { IMAGE_SERVICE_TOKEN, LOGGER_TOKEN, TEAM_STATS_REPOSITORY_TOKEN, MEDIA_REPOSITORY_TOKEN } from './TeamTokens';
import { IMAGE_SERVICE_TOKEN, LOGGER_TOKEN, MEDIA_REPOSITORY_TOKEN } from './TeamTokens';
export {
TEAM_REPOSITORY_TOKEN,
@@ -8,18 +8,15 @@ export {
DRIVER_REPOSITORY_TOKEN,
IMAGE_SERVICE_TOKEN,
LOGGER_TOKEN,
TEAM_STATS_REPOSITORY_TOKEN,
MEDIA_REPOSITORY_TOKEN,
} from './TeamTokens';
// Import core interfaces
import type { Logger } from '@core/shared/application/Logger';
import type { ITeamStatsRepository } from '@core/racing/domain/repositories/ITeamStatsRepository';
// Import concrete in-memory implementations
import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter';
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
import { InMemoryTeamStatsRepository } from '@adapters/racing/persistence/inmemory/InMemoryTeamStatsRepository';
import { InMemoryMediaRepository } from '@adapters/racing/persistence/media/InMemoryMediaRepository';
// Import presenters
@@ -35,11 +32,6 @@ export const TeamProviders: Provider[] = [
provide: LOGGER_TOKEN,
useClass: ConsoleLogger,
},
{
provide: TEAM_STATS_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryTeamStatsRepository(logger),
inject: [LOGGER_TOKEN],
},
{
provide: MEDIA_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryMediaRepository(logger),
@@ -47,7 +39,6 @@ export const TeamProviders: Provider[] = [
},
{
provide: AllTeamsPresenter,
useFactory: (teamStatsRepository: ITeamStatsRepository) => new AllTeamsPresenter(teamStatsRepository),
inject: [TEAM_STATS_REPOSITORY_TOKEN],
useFactory: () => new AllTeamsPresenter(),
},
];
];

View File

@@ -113,7 +113,6 @@ describe('TeamService', () => {
const teamStatsRepository = {
getTeamStats: vi.fn(),
getTeamStatsSync: vi.fn(),
saveTeamStats: vi.fn(),
getAllStats: vi.fn(),
clear: vi.fn(),
@@ -126,6 +125,10 @@ describe('TeamService', () => {
saveDriverAvatar: vi.fn(),
};
const resultRepository = {
findAll: vi.fn(),
};
const allTeamsPresenter = {
reset: vi.fn(),
present: vi.fn(),
@@ -140,6 +143,7 @@ describe('TeamService', () => {
logger,
teamStatsRepository as unknown as never,
mediaRepository as unknown as never,
resultRepository as unknown as never,
allTeamsPresenter as unknown as never
);
});
@@ -558,4 +562,4 @@ describe('TeamService', () => {
executeSpy.mockRestore();
});
});
});

View File

@@ -37,9 +37,10 @@ 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, MEDIA_REPOSITORY_TOKEN } from './TeamTokens';
import { TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, LOGGER_TOKEN, TEAM_STATS_REPOSITORY_TOKEN, MEDIA_REPOSITORY_TOKEN, RESULT_REPOSITORY_TOKEN } from './TeamTokens';
import type { ITeamStatsRepository } from '@core/racing/domain/repositories/ITeamStatsRepository';
import type { IMediaRepository } from '@core/racing/domain/repositories/IMediaRepository';
import type { IResultRepository } from '@core/racing/domain/repositories/IResultRepository';
@Injectable()
export class TeamService {
@@ -50,6 +51,7 @@ export class TeamService {
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
@Inject(TEAM_STATS_REPOSITORY_TOKEN) private readonly teamStatsRepository: ITeamStatsRepository,
@Inject(MEDIA_REPOSITORY_TOKEN) private readonly mediaRepository: IMediaRepository,
@Inject(RESULT_REPOSITORY_TOKEN) private readonly resultRepository: IResultRepository,
private readonly allTeamsPresenter: AllTeamsPresenter,
) {}
@@ -61,6 +63,7 @@ export class TeamService {
this.membershipRepository,
this.teamStatsRepository,
this.mediaRepository,
this.resultRepository,
this.logger,
this.allTeamsPresenter
);
@@ -174,13 +177,13 @@ export class TeamService {
}
async getDriverTeam(driverId: string): Promise<GetDriverTeamOutputDTO | null> {
this.logger.debug(`[TeamService] Fetching driver team for driverId: ${driverId}`);
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 result = await useCase.execute({ driverId });
if (result.isErr()) {
this.logger.error(`Error fetching driver team for driverId: ${driverId}: ${result.error?.details?.message || 'Unknown error'}`);
this.logger.error(`Error fetching team for driverId: ${driverId}: ${result.error?.details?.message || 'Unknown error'}`);
return null;
}

View File

@@ -4,4 +4,5 @@ export const DRIVER_REPOSITORY_TOKEN = 'IDriverRepository';
export const IMAGE_SERVICE_TOKEN = 'IImageServicePort';
export const LOGGER_TOKEN = 'Logger';
export const TEAM_STATS_REPOSITORY_TOKEN = 'ITeamStatsRepository';
export const MEDIA_REPOSITORY_TOKEN = 'IMediaRepository';
export const MEDIA_REPOSITORY_TOKEN = 'IMediaRepository';
export const RESULT_REPOSITORY_TOKEN = 'IResultRepository';

View File

@@ -326,6 +326,15 @@ export class TeamDTO {
@IsArray()
leagues!: string[];
@ApiProperty({ required: false })
@IsOptional()
@IsString()
category?: string | undefined;
@ApiProperty()
@IsBoolean()
isRecruiting!: boolean;
@ApiProperty({ required: false })
@IsOptional()
@IsString()

View File

@@ -37,10 +37,16 @@ export class TeamListItemDTO {
@ApiProperty({ required: false, enum: ['beginner', 'intermediate', 'advanced', 'pro'] })
performanceLevel?: 'beginner' | 'intermediate' | 'advanced' | 'pro';
@ApiProperty({ required: false })
category?: string | undefined;
@ApiProperty({ required: false })
logoUrl?: string;
@ApiProperty({ required: false })
rating?: number;
@ApiProperty()
isRecruiting!: boolean;
}

View File

@@ -1,53 +1,40 @@
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { GetAllTeamsResult } from '@core/racing/application/use-cases/GetAllTeamsUseCase';
import { GetAllTeamsOutputDTO } from '../dtos/GetAllTeamsOutputDTO';
import type { ITeamStatsRepository } from '@core/racing/domain/repositories/ITeamStatsRepository';
import { TeamListItemDTO } from '../dtos/TeamListItemDTO';
export class AllTeamsPresenter implements UseCaseOutputPort<GetAllTeamsResult> {
private model: GetAllTeamsOutputDTO | null = null;
constructor(
private readonly teamStatsRepository: ITeamStatsRepository
) {}
reset(): void {
this.model = null;
}
present(result: GetAllTeamsResult): void {
const teams: TeamListItemDTO[] = result.teams.map(team => {
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 ?? [];
// Return relative URL for proxying through Next.js rewrites
dto.logoUrl = `/api/media/teams/${team.id}/logo`;
dto.rating = team.rating ?? 0;
dto.category = team.category;
dto.isRecruiting = team.isRecruiting;
return dto;
});
this.model = {
teams: result.teams.map(team => {
const stats = this.teamStatsRepository.getTeamStatsSync(team.id.toString());
return {
id: team.id,
name: team.name.toString(),
tag: team.tag.toString(),
description: team.description?.toString() || '',
memberCount: team.memberCount,
leagues: team.leagues?.map(l => l.toString()) || [],
// Add stats fields
...(stats ? {
totalWins: stats.totalWins,
totalRaces: stats.totalRaces,
performanceLevel: stats.performanceLevel,
specialization: stats.specialization,
region: stats.region,
languages: stats.languages,
logoUrl: stats.logoUrl,
rating: stats.rating,
} : {
totalWins: 0,
totalRaces: 0,
performanceLevel: 'beginner',
specialization: 'mixed',
region: '',
languages: [],
logoUrl: '',
rating: 0,
}),
};
}),
teams,
totalCount: result.totalCount ?? result.teams.length,
};
}

View File

@@ -21,6 +21,7 @@ export class DriverTeamPresenter implements UseCaseOutputPort<GetDriverTeamResul
description: result.team.description?.toString() || '',
ownerId: result.team.ownerId.toString(),
leagues: result.team.leagues?.map(l => l.toString()) || [],
isRecruiting: result.team.isRecruiting,
createdAt: result.team.createdAt.toDate().toISOString(),
},
membership: {

View File

@@ -18,6 +18,8 @@ export class TeamDetailsPresenter implements UseCaseOutputPort<GetTeamDetailsRes
description: result.team.description?.toString() || '',
ownerId: result.team.ownerId.toString(),
leagues: result.team.leagues?.map(l => l.toString()) || [],
category: result.team.category,
isRecruiting: result.team.isRecruiting,
createdAt: result.team.createdAt.toDate().toISOString(),
},
membership: result.membership

View File

@@ -55,6 +55,7 @@ import {
TeamMembershipOrmEntity,
TeamOrmEntity,
} from '@adapters/racing/persistence/typeorm/entities/TeamOrmEntities';
import { TeamStatsOrmEntity } from '@adapters/racing/persistence/typeorm/entities/TeamStatsOrmEntity';
import { TypeOrmDriverRepository } from '@adapters/racing/persistence/typeorm/repositories/TypeOrmDriverRepository';
import { TypeOrmLeagueMembershipRepository } from '@adapters/racing/persistence/typeorm/repositories/TypeOrmLeagueMembershipRepository';
@@ -79,9 +80,11 @@ import { TypeOrmTeamMembershipRepository, TypeOrmTeamRepository } from '@adapter
// Import in-memory implementations for new repositories (TypeORM versions not yet implemented)
import { InMemoryDriverStatsRepository } from '@adapters/racing/persistence/inmemory/InMemoryDriverStatsRepository';
import { InMemoryTeamStatsRepository } from '@adapters/racing/persistence/inmemory/InMemoryTeamStatsRepository';
import { InMemoryMediaRepository } from '@adapters/racing/persistence/media/InMemoryMediaRepository';
// Import TypeORM repository for team stats
import { TypeOrmTeamStatsRepository } from '@adapters/racing/persistence/typeorm/repositories/TypeOrmTeamStatsRepository';
import { DriverOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/DriverOrmMapper';
import { LeagueMembershipOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/LeagueMembershipOrmMapper';
import { LeagueOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/LeagueOrmMapper';
@@ -105,6 +108,7 @@ import {
import { MoneyOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/MoneyOrmMapper';
import { PenaltyOrmMapper, ProtestOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/StewardingOrmMappers';
import { TeamMembershipOrmMapper, TeamOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/TeamOrmMappers';
import { TeamStatsOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/TeamStatsOrmMapper';
import { getPointsSystems } from '@adapters/bootstrap/PointsSystems';
import type { Logger } from '@core/shared/application/Logger';
@@ -126,6 +130,7 @@ const typeOrmFeatureImports = [
TeamOrmEntity,
TeamMembershipOrmEntity,
TeamJoinRequestOrmEntity,
TeamStatsOrmEntity,
PenaltyOrmEntity,
ProtestOrmEntity,
@@ -155,6 +160,7 @@ const typeOrmFeatureImports = [
{ provide: TeamOrmMapper, useFactory: () => new TeamOrmMapper() },
{ provide: TeamMembershipOrmMapper, useFactory: () => new TeamMembershipOrmMapper() },
{ provide: TeamStatsOrmMapper, useFactory: () => new TeamStatsOrmMapper() },
{ provide: PenaltyOrmMapper, useFactory: () => new PenaltyOrmMapper() },
{ provide: ProtestOrmMapper, useFactory: () => new ProtestOrmMapper() },
@@ -321,8 +327,8 @@ const typeOrmFeatureImports = [
},
{
provide: TEAM_STATS_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryTeamStatsRepository(logger),
inject: ['Logger'],
useFactory: (repo: Repository<TeamStatsOrmEntity>, mapper: TeamStatsOrmMapper) => new TypeOrmTeamStatsRepository(repo, mapper),
inject: [getRepositoryToken(TeamStatsOrmEntity), TeamStatsOrmMapper],
},
{
provide: MEDIA_REPOSITORY_TOKEN,
@@ -356,4 +362,4 @@ const typeOrmFeatureImports = [
MEDIA_REPOSITORY_TOKEN,
],
})
export class PostgresRacingPersistenceModule {}
export class PostgresRacingPersistenceModule {}