This commit is contained in:
2025-12-09 22:22:06 +01:00
parent e34a11ae7c
commit 3adf2e5e94
62 changed files with 6079 additions and 998 deletions

View File

@@ -40,6 +40,20 @@ import type { IFeedRepository } from '@gridpilot/social/domain/repositories/IFee
import type { ISocialGraphRepository } from '@gridpilot/social/domain/repositories/ISocialGraphRepository';
import type { ImageServicePort } from '@gridpilot/media';
// Notifications package imports
import type { INotificationRepository, INotificationPreferenceRepository } from '@gridpilot/notifications/application';
import {
SendNotificationUseCase,
MarkNotificationReadUseCase,
GetUnreadNotificationsQuery
} from '@gridpilot/notifications/application';
import {
InMemoryNotificationRepository,
InMemoryNotificationPreferenceRepository,
NotificationGatewayRegistry,
InAppNotificationAdapter,
} from '@gridpilot/notifications/infrastructure';
import { InMemoryDriverRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryDriverRepository';
import { InMemoryLeagueRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryLeagueRepository';
import { InMemoryRaceRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryRaceRepository';
@@ -92,7 +106,10 @@ import {
ApplyPenaltyUseCase,
GetRaceProtestsQuery,
GetRacePenaltiesQuery,
RequestProtestDefenseUseCase,
SubmitProtestDefenseUseCase,
} from '@gridpilot/racing/application';
import { TransferLeagueOwnershipUseCase } from '@gridpilot/racing/application/use-cases/TransferLeagueOwnershipUseCase';
import type { DriverRatingProvider } from '@gridpilot/racing/application';
import {
createStaticRacingSeed,
@@ -194,6 +211,14 @@ class DIContainer {
private _trackRepository: ITrackRepository;
private _carRepository: ICarRepository;
// Notifications
private _notificationRepository: INotificationRepository;
private _notificationPreferenceRepository: INotificationPreferenceRepository;
private _notificationGatewayRegistry: NotificationGatewayRegistry;
private _sendNotificationUseCase: SendNotificationUseCase;
private _markNotificationReadUseCase: MarkNotificationReadUseCase;
private _getUnreadNotificationsQuery: GetUnreadNotificationsQuery;
// Racing application use-cases / queries
private _joinLeagueUseCase: JoinLeagueUseCase;
private _registerForRaceUseCase: RegisterForRaceUseCase;
@@ -219,6 +244,8 @@ class DIContainer {
private _applyPenaltyUseCase: ApplyPenaltyUseCase;
private _getRaceProtestsQuery: GetRaceProtestsQuery;
private _getRacePenaltiesQuery: GetRacePenaltiesQuery;
private _requestProtestDefenseUseCase: RequestProtestDefenseUseCase;
private _submitProtestDefenseUseCase: SubmitProtestDefenseUseCase;
private _createTeamUseCase: CreateTeamUseCase;
private _joinTeamUseCase: JoinTeamUseCase;
@@ -231,6 +258,7 @@ class DIContainer {
private _getTeamMembersQuery: GetTeamMembersQuery;
private _getTeamJoinRequestsQuery: GetTeamJoinRequestsQuery;
private _getDriverTeamQuery: GetDriverTeamQuery;
private _transferLeagueOwnershipUseCase: TransferLeagueOwnershipUseCase;
private constructor() {
// Create seed data
@@ -435,8 +463,7 @@ class DIContainer {
for (const league of seedData.leagues) {
const archetype = getDemoLeagueArchetypeByName(league.name);
if (!archetype) continue;
const season = Season.create({
id: `season-${league.id}-demo`,
leagueId: league.id,
@@ -450,15 +477,14 @@ class DIContainer {
});
seededSeasons.push(season);
const infraPreset = getLeagueScoringPresetById(
archetype.scoringPresetId,
);
if (!infraPreset) {
// If a preset is missing, skip scoring config for this league in alpha seed.
continue;
// Use archetype preset if available, otherwise fall back to default club preset
const presetId = archetype?.scoringPresetId ?? 'club-default';
const infraPreset = getLeagueScoringPresetById(presetId);
if (infraPreset) {
const config = infraPreset.createConfig({ seasonId: season.id });
seededScoringConfigs.push(config);
}
const config = infraPreset.createConfig({ seasonId: season.id });
seededScoringConfigs.push(config);
}
this._gameRepository = new InMemoryGameRepository([game]);
@@ -550,6 +576,33 @@ class DIContainer {
});
}
// Seed sample league stewards for the primary driver's league (alpha demo)
if (primaryLeagueForAdmins) {
const stewardCandidates = seedData.drivers
.filter((d) => d.id !== primaryLeagueForAdmins.ownerId)
.slice(2, 5);
stewardCandidates.forEach((driver) => {
const existing = seededMemberships.find(
(m) =>
m.leagueId === primaryLeagueForAdmins.id && m.driverId === driver.id,
);
if (existing) {
if (existing.role !== 'owner' && existing.role !== 'admin') {
existing.role = 'steward';
}
} else {
seededMemberships.push({
leagueId: primaryLeagueForAdmins.id,
driverId: driver.id,
role: 'steward',
status: 'active',
joinedAt: new Date(),
});
}
});
}
// Seed a few pending join requests for demo leagues (expanded to more leagues)
const seededJoinRequests: JoinRequest[] = [];
const demoLeagues = seedData.leagues.slice(0, 6); // Expanded from 2 to 6 leagues
@@ -762,6 +815,11 @@ class DIContainer {
this._teamMembershipRepository,
);
this._transferLeagueOwnershipUseCase = new TransferLeagueOwnershipUseCase(
this._leagueRepository,
this._leagueMembershipRepository,
);
// Stewarding use cases and queries
this._fileProtestUseCase = new FileProtestUseCase(
this._protestRepository,
@@ -787,6 +845,14 @@ class DIContainer {
this._penaltyRepository,
this._driverRepository,
);
this._requestProtestDefenseUseCase = new RequestProtestDefenseUseCase(
this._protestRepository,
this._raceRepository,
this._leagueMembershipRepository,
);
this._submitProtestDefenseUseCase = new SubmitProtestDefenseUseCase(
this._protestRepository,
);
// Social and feed adapters backed by static seed
this._feedRepository = new InMemoryFeedRepository(seedData);
@@ -972,6 +1038,29 @@ class DIContainer {
this._trackRepository = new InMemoryTrackRepository(seedTracks);
this._carRepository = new InMemoryCarRepository(seedCars);
// Initialize notifications
this._notificationRepository = new InMemoryNotificationRepository();
this._notificationPreferenceRepository = new InMemoryNotificationPreferenceRepository();
// Set up gateway registry with adapters
this._notificationGatewayRegistry = new NotificationGatewayRegistry([
new InAppNotificationAdapter(),
// Future: DiscordNotificationAdapter, EmailNotificationAdapter
]);
// Notification use cases
this._sendNotificationUseCase = new SendNotificationUseCase(
this._notificationRepository,
this._notificationPreferenceRepository,
this._notificationGatewayRegistry,
);
this._markNotificationReadUseCase = new MarkNotificationReadUseCase(
this._notificationRepository,
);
this._getUnreadNotificationsQuery = new GetUnreadNotificationsQuery(
this._notificationRepository,
);
}
/**
@@ -1187,6 +1276,26 @@ class DIContainer {
return this._carRepository;
}
get notificationRepository(): INotificationRepository {
return this._notificationRepository;
}
get notificationPreferenceRepository(): INotificationPreferenceRepository {
return this._notificationPreferenceRepository;
}
get sendNotificationUseCase(): SendNotificationUseCase {
return this._sendNotificationUseCase;
}
get markNotificationReadUseCase(): MarkNotificationReadUseCase {
return this._markNotificationReadUseCase;
}
get getUnreadNotificationsQuery(): GetUnreadNotificationsQuery {
return this._getUnreadNotificationsQuery;
}
get fileProtestUseCase(): FileProtestUseCase {
return this._fileProtestUseCase;
}
@@ -1206,6 +1315,18 @@ class DIContainer {
get getRacePenaltiesQuery(): GetRacePenaltiesQuery {
return this._getRacePenaltiesQuery;
}
get requestProtestDefenseUseCase(): RequestProtestDefenseUseCase {
return this._requestProtestDefenseUseCase;
}
get submitProtestDefenseUseCase(): SubmitProtestDefenseUseCase {
return this._submitProtestDefenseUseCase;
}
get transferLeagueOwnershipUseCase(): TransferLeagueOwnershipUseCase {
return this._transferLeagueOwnershipUseCase;
}
}
/**
@@ -1388,6 +1509,26 @@ export function getCarRepository(): ICarRepository {
return DIContainer.getInstance().carRepository;
}
export function getNotificationRepository(): INotificationRepository {
return DIContainer.getInstance().notificationRepository;
}
export function getNotificationPreferenceRepository(): INotificationPreferenceRepository {
return DIContainer.getInstance().notificationPreferenceRepository;
}
export function getSendNotificationUseCase(): SendNotificationUseCase {
return DIContainer.getInstance().sendNotificationUseCase;
}
export function getMarkNotificationReadUseCase(): MarkNotificationReadUseCase {
return DIContainer.getInstance().markNotificationReadUseCase;
}
export function getGetUnreadNotificationsQuery(): GetUnreadNotificationsQuery {
return DIContainer.getInstance().getUnreadNotificationsQuery;
}
export function getFileProtestUseCase(): FileProtestUseCase {
return DIContainer.getInstance().fileProtestUseCase;
}
@@ -1408,6 +1549,18 @@ export function getGetRacePenaltiesQuery(): GetRacePenaltiesQuery {
return DIContainer.getInstance().getRacePenaltiesQuery;
}
export function getRequestProtestDefenseUseCase(): RequestProtestDefenseUseCase {
return DIContainer.getInstance().requestProtestDefenseUseCase;
}
export function getSubmitProtestDefenseUseCase(): SubmitProtestDefenseUseCase {
return DIContainer.getInstance().submitProtestDefenseUseCase;
}
export function getTransferLeagueOwnershipUseCase(): TransferLeagueOwnershipUseCase {
return DIContainer.getInstance().transferLeagueOwnershipUseCase;
}
/**
* Reset function for testing
*/

View File

@@ -5,7 +5,7 @@ import type {
MembershipRole,
MembershipStatus,
} from '@gridpilot/racing/domain/entities/LeagueMembership';
import { leagues, memberships as seedMemberships } from '@gridpilot/testing-support';
import { leagues, memberships as seedMemberships, drivers } from '@gridpilot/testing-support';
/**
* Lightweight league membership model mirroring the domain type but with
@@ -65,6 +65,63 @@ const leagueMemberships = new Map<string, LeagueMembership[]>();
byLeague.set(league.id, list);
}
// Seed sample league admins for the primary driver's league (alpha demo)
const primaryDriverId = drivers[0]?.id ?? 'driver-1';
const primaryLeagueForAdmins = leagues.find((l) => l.ownerId === primaryDriverId) ?? leagues[0];
if (primaryLeagueForAdmins) {
const adminCandidates = drivers
.filter((d) => d.id !== primaryLeagueForAdmins.ownerId)
.slice(0, 2);
adminCandidates.forEach((driver) => {
const list = byLeague.get(primaryLeagueForAdmins.id) ?? [];
const existing = list.find((m) => m.driverId === driver.id);
if (existing) {
if (existing.role !== 'owner') {
existing.role = 'admin';
}
} else {
const joinedAt = new Date().toISOString();
list.push({
leagueId: primaryLeagueForAdmins.id,
driverId: driver.id,
role: 'admin',
status: 'active',
joinedAt,
});
}
byLeague.set(primaryLeagueForAdmins.id, list);
});
}
// Seed sample league stewards for the primary driver's league (alpha demo)
if (primaryLeagueForAdmins) {
const stewardCandidates = drivers
.filter((d) => d.id !== primaryLeagueForAdmins.ownerId)
.slice(2, 5);
stewardCandidates.forEach((driver) => {
const list = byLeague.get(primaryLeagueForAdmins.id) ?? [];
const existing = list.find((m) => m.driverId === driver.id);
if (existing) {
if (existing.role !== 'owner' && existing.role !== 'admin') {
existing.role = 'steward';
}
} else {
const joinedAt = new Date().toISOString();
list.push({
leagueId: primaryLeagueForAdmins.id,
driverId: driver.id,
role: 'steward',
status: 'active',
joinedAt,
});
}
byLeague.set(primaryLeagueForAdmins.id, list);
});
}
for (const [leagueId, list] of byLeague.entries()) {
leagueMemberships.set(leagueId, list);
}