Files
gridpilot.gg/apps/website/lib/services/LeagueMembershipService.ts
2025-12-16 00:57:31 +01:00

101 lines
3.5 KiB
TypeScript

import type { ILeagueMembershipRepository } from '@gridpilot/racing/domain/repositories/ILeagueMembershipRepository';
import type { ILeagueRepository } from '@gridpilot/racing/domain/repositories/ILeagueRepository';
import type {
LeagueMembership as DomainLeagueMembership,
MembershipRole,
MembershipStatus,
} from '@gridpilot/racing/domain/entities/LeagueMembership';
/**
* Lightweight league membership model mirroring the domain type but with
* a stringified joinedAt for easier UI formatting.
*/
export interface LeagueMembership extends Omit<DomainLeagueMembership, 'joinedAt'> {
joinedAt: string;
}
export class LeagueMembershipService {
private leagueMemberships = new Map<string, LeagueMembership[]>();
constructor(
private readonly leagueRepository: ILeagueRepository,
private readonly membershipRepository: ILeagueMembershipRepository,
) {
this.initializeLeagueMembershipsFromRepository();
}
/**
* Initialize league memberships once from the in-memory league membership repository
* that is seeded via the static racing seed in the DI container.
*
* This avoids depending on raw testing-support seed exports and keeps all demo
* membership data flowing through the same in-memory repositories used elsewhere.
*/
private async initializeLeagueMembershipsFromRepository() {
if (this.leagueMemberships.size > 0) {
return;
}
try {
const allLeagues = await this.leagueRepository.findAll();
const byLeague = new Map<string, LeagueMembership[]>();
for (const league of allLeagues) {
const memberships = await this.membershipRepository.getLeagueMembers(league.id);
const mapped: LeagueMembership[] = memberships.map((membership) => ({
id: membership.id,
leagueId: membership.leagueId,
driverId: membership.driverId,
role: membership.role,
status: membership.status,
joinedAt:
membership.joinedAt instanceof Date
? membership.joinedAt.toISOString()
: new Date().toISOString(),
}));
byLeague.set(league.id, mapped);
}
for (const [leagueId, list] of byLeague.entries()) {
this.leagueMemberships.set(leagueId, list);
}
} catch (error) {
// In alpha/demo mode we tolerate failures here; callers will see empty memberships.
// eslint-disable-next-line no-console
console.error('Failed to initialize league memberships from repository', error);
}
}
getMembership(leagueId: string, driverId: string): LeagueMembership | null {
const list = this.leagueMemberships.get(leagueId);
if (!list) return null;
return list.find((m) => m.driverId === driverId) ?? null;
}
getLeagueMembers(leagueId: string): LeagueMembership[] {
return [...(this.leagueMemberships.get(leagueId) ?? [])];
}
/**
* Derive a driver's primary league from in-memory league memberships.
* Prefers any active membership and returns the first matching league.
*/
getPrimaryLeagueIdForDriver(driverId: string): string | null {
for (const [leagueId, members] of this.leagueMemberships.entries()) {
if (members.some((m) => m.driverId === driverId && m.status === 'active')) {
return leagueId;
}
}
return null;
}
isOwnerOrAdmin(leagueId: string, driverId: string): boolean {
const membership = this.getMembership(leagueId, driverId);
if (!membership) return false;
return membership.role === 'owner' || membership.role === 'admin';
}
}
export type { MembershipRole, MembershipStatus };