This commit is contained in:
2025-12-10 12:38:55 +01:00
parent 0f7fe67d3c
commit fbbcf414a4
87 changed files with 11972 additions and 390 deletions

View File

@@ -0,0 +1,170 @@
/**
* Infrastructure Adapter: InMemoryAchievementRepository
*
* In-memory implementation of IAchievementRepository
*/
import {
Achievement,
AchievementCategory,
DRIVER_ACHIEVEMENTS,
STEWARD_ACHIEVEMENTS,
ADMIN_ACHIEVEMENTS,
COMMUNITY_ACHIEVEMENTS,
} from '../../domain/entities/Achievement';
import { UserAchievement } from '../../domain/entities/UserAchievement';
import type { IAchievementRepository } from '../../domain/repositories/IAchievementRepository';
export class InMemoryAchievementRepository implements IAchievementRepository {
private achievements: Map<string, Achievement> = new Map();
private userAchievements: Map<string, UserAchievement> = new Map();
constructor() {
// Seed with predefined achievements
this.seedAchievements();
}
private seedAchievements(): void {
const allAchievements = [
...DRIVER_ACHIEVEMENTS,
...STEWARD_ACHIEVEMENTS,
...ADMIN_ACHIEVEMENTS,
...COMMUNITY_ACHIEVEMENTS,
];
for (const props of allAchievements) {
const achievement = Achievement.create(props);
this.achievements.set(achievement.id, achievement);
}
}
// Achievement operations
async findAchievementById(id: string): Promise<Achievement | null> {
return this.achievements.get(id) ?? null;
}
async findAllAchievements(): Promise<Achievement[]> {
return Array.from(this.achievements.values());
}
async findAchievementsByCategory(category: AchievementCategory): Promise<Achievement[]> {
return Array.from(this.achievements.values())
.filter(a => a.category === category);
}
async createAchievement(achievement: Achievement): Promise<Achievement> {
if (this.achievements.has(achievement.id)) {
throw new Error('Achievement with this ID already exists');
}
this.achievements.set(achievement.id, achievement);
return achievement;
}
// UserAchievement operations
async findUserAchievementById(id: string): Promise<UserAchievement | null> {
return this.userAchievements.get(id) ?? null;
}
async findUserAchievementsByUserId(userId: string): Promise<UserAchievement[]> {
return Array.from(this.userAchievements.values())
.filter(ua => ua.userId === userId);
}
async findUserAchievementByUserAndAchievement(
userId: string,
achievementId: string
): Promise<UserAchievement | null> {
for (const ua of this.userAchievements.values()) {
if (ua.userId === userId && ua.achievementId === achievementId) {
return ua;
}
}
return null;
}
async hasUserEarnedAchievement(userId: string, achievementId: string): Promise<boolean> {
const ua = await this.findUserAchievementByUserAndAchievement(userId, achievementId);
return ua !== null && ua.isComplete();
}
async createUserAchievement(userAchievement: UserAchievement): Promise<UserAchievement> {
if (this.userAchievements.has(userAchievement.id)) {
throw new Error('UserAchievement with this ID already exists');
}
this.userAchievements.set(userAchievement.id, userAchievement);
return userAchievement;
}
async updateUserAchievement(userAchievement: UserAchievement): Promise<UserAchievement> {
if (!this.userAchievements.has(userAchievement.id)) {
throw new Error('UserAchievement not found');
}
this.userAchievements.set(userAchievement.id, userAchievement);
return userAchievement;
}
// Stats
async getAchievementLeaderboard(limit: number): Promise<{ userId: string; points: number; count: number }[]> {
const userStats = new Map<string, { points: number; count: number }>();
for (const ua of this.userAchievements.values()) {
if (!ua.isComplete()) continue;
const achievement = this.achievements.get(ua.achievementId);
if (!achievement) continue;
const existing = userStats.get(ua.userId) ?? { points: 0, count: 0 };
userStats.set(ua.userId, {
points: existing.points + achievement.points,
count: existing.count + 1,
});
}
return Array.from(userStats.entries())
.map(([userId, stats]) => ({ userId, ...stats }))
.sort((a, b) => b.points - a.points)
.slice(0, limit);
}
async getUserAchievementStats(userId: string): Promise<{
total: number;
points: number;
byCategory: Record<AchievementCategory, number>
}> {
const userAchievements = await this.findUserAchievementsByUserId(userId);
const completedAchievements = userAchievements.filter(ua => ua.isComplete());
const byCategory: Record<AchievementCategory, number> = {
driver: 0,
steward: 0,
admin: 0,
community: 0,
};
let points = 0;
for (const ua of completedAchievements) {
const achievement = this.achievements.get(ua.achievementId);
if (achievement) {
points += achievement.points;
byCategory[achievement.category]++;
}
}
return {
total: completedAchievements.length,
points,
byCategory,
};
}
// Test helpers
clearUserAchievements(): void {
this.userAchievements.clear();
}
clear(): void {
this.achievements.clear();
this.userAchievements.clear();
}
}

View File

@@ -0,0 +1,48 @@
/**
* Infrastructure: InMemorySponsorAccountRepository
*
* In-memory implementation of ISponsorAccountRepository for development/testing.
*/
import type { ISponsorAccountRepository } from '../../domain/repositories/ISponsorAccountRepository';
import type { SponsorAccount } from '../../domain/entities/SponsorAccount';
import type { UserId } from '../../domain/value-objects/UserId';
export class InMemorySponsorAccountRepository implements ISponsorAccountRepository {
private accounts: Map<string, SponsorAccount> = new Map();
async save(account: SponsorAccount): Promise<void> {
this.accounts.set(account.getId().value, account);
}
async findById(id: UserId): Promise<SponsorAccount | null> {
return this.accounts.get(id.value) ?? null;
}
async findBySponsorId(sponsorId: string): Promise<SponsorAccount | null> {
return Array.from(this.accounts.values()).find(
a => a.getSponsorId() === sponsorId
) ?? null;
}
async findByEmail(email: string): Promise<SponsorAccount | null> {
const normalizedEmail = email.toLowerCase().trim();
return Array.from(this.accounts.values()).find(
a => a.getEmail().toLowerCase() === normalizedEmail
) ?? null;
}
async delete(id: UserId): Promise<void> {
this.accounts.delete(id.value);
}
// Helper for testing
clear(): void {
this.accounts.clear();
}
// Helper for seeding demo data
seed(accounts: SponsorAccount[]): void {
accounts.forEach(a => this.accounts.set(a.getId().value, a));
}
}

View File

@@ -0,0 +1,65 @@
/**
* Infrastructure Adapter: InMemoryUserRatingRepository
*
* In-memory implementation of IUserRatingRepository
*/
import { UserRating } from '../../domain/value-objects/UserRating';
import type { IUserRatingRepository } from '../../domain/repositories/IUserRatingRepository';
export class InMemoryUserRatingRepository implements IUserRatingRepository {
private ratings: Map<string, UserRating> = new Map();
async findByUserId(userId: string): Promise<UserRating | null> {
return this.ratings.get(userId) ?? null;
}
async findByUserIds(userIds: string[]): Promise<UserRating[]> {
const results: UserRating[] = [];
for (const userId of userIds) {
const rating = this.ratings.get(userId);
if (rating) {
results.push(rating);
}
}
return results;
}
async save(rating: UserRating): Promise<UserRating> {
this.ratings.set(rating.userId, rating);
return rating;
}
async getTopDrivers(limit: number): Promise<UserRating[]> {
return Array.from(this.ratings.values())
.filter(r => r.driver.sampleSize > 0)
.sort((a, b) => b.driver.value - a.driver.value)
.slice(0, limit);
}
async getTopTrusted(limit: number): Promise<UserRating[]> {
return Array.from(this.ratings.values())
.filter(r => r.trust.sampleSize > 0)
.sort((a, b) => b.trust.value - a.trust.value)
.slice(0, limit);
}
async getEligibleStewards(): Promise<UserRating[]> {
return Array.from(this.ratings.values())
.filter(r => r.canBeSteward());
}
async findByDriverTier(tier: 'rookie' | 'amateur' | 'semi-pro' | 'pro' | 'elite'): Promise<UserRating[]> {
return Array.from(this.ratings.values())
.filter(r => r.getDriverTier() === tier);
}
async delete(userId: string): Promise<void> {
this.ratings.delete(userId);
}
// Test helper
clear(): void {
this.ratings.clear();
}
}