diff --git a/src/app.module.ts b/src/app.module.ts index 18d39f7..3b45767 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -42,6 +42,7 @@ import { QuestsModule } from './quests/quests.module'; import { IntegrationsModule } from './integrations/integrations.module'; import { BlockchainTransactionModule } from './blockchain-transaction/blockchain-transaction.module'; import { PrivacyModule } from './privacy/privacy.module'; +import { DailyChallengesModule } from './daily-challenges/daily-challenges.module'; import { EnergyModule } from './energy/energy.module'; import { SkillRatingModule } from './skill-rating/skill-rating.module'; import { WalletAuthModule } from './auth/wallet-auth.module'; @@ -129,6 +130,7 @@ import { WalletAuthModule } from './auth/wallet-auth.module'; IntegrationsModule, BlockchainTransactionModule, PrivacyModule, + DailyChallengesModule, SkillRatingModule, WalletAuthModule, ], diff --git a/src/daily-challenges/controllers/daily-challenges.controller.ts b/src/daily-challenges/controllers/daily-challenges.controller.ts new file mode 100644 index 0000000..dbb05f2 --- /dev/null +++ b/src/daily-challenges/controllers/daily-challenges.controller.ts @@ -0,0 +1,53 @@ +import { + Controller, + Get, + Post, + Body, + Param, + UseGuards, + Query, + Req, +} from '@nestjs/common'; +import { DailyChallengesService } from '../services/daily-challenges.service'; +import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'; +import { Request } from 'express'; + +// Ensure the Request type has user property injected by passport-jwt +interface AuthenticatedRequest extends Request { + user: { id: string; email: string; role: string }; +} + +@Controller('daily-challenges') +@UseGuards(JwtAuthGuard) +export class DailyChallengesController { + constructor( + private readonly dailyChallengesService: DailyChallengesService, + ) {} + + @Get('today') + async getTodayChallenge(@Req() req: AuthenticatedRequest) { + const userId = req.user.id; + return this.dailyChallengesService.getActiveChallenge(userId); + } + + @Post(':id/complete') + async completeChallenge( + @Param('id') challengeId: string, + @Body() body: { score: number; timeSpent: number }, + @Req() req: AuthenticatedRequest, + ) { + return this.dailyChallengesService.completeChallenge( + req.user.id, + challengeId, + body, + ); + } + + @Get('history') + async getHistory( + @Req() req: AuthenticatedRequest, + @Query('limit') limit: number, + ) { + return this.dailyChallengesService.getHistory(req.user.id, limit || 30); + } +} diff --git a/src/daily-challenges/daily-challenges.module.ts b/src/daily-challenges/daily-challenges.module.ts new file mode 100644 index 0000000..1845dfc --- /dev/null +++ b/src/daily-challenges/daily-challenges.module.ts @@ -0,0 +1,30 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ScheduleModule } from '@nestjs/schedule'; + +import { DailyChallenge } from './entities/daily-challenge.entity'; +import { DailyChallengeCompletion } from './entities/daily-challenge-completion.entity'; +import { DailyChallengesService } from './services/daily-challenges.service'; +import { ChallengeRotationCron } from './services/challenge-rotation.cron'; +import { DailyChallengesController } from './controllers/daily-challenges.controller'; + +import { Puzzle } from '../puzzles/entities/puzzle.entity'; +import { UserStreak } from '../users/entities/user-streak.entity'; +import { User } from '../users/entities/user.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + DailyChallenge, + DailyChallengeCompletion, + Puzzle, + UserStreak, + User, + ]), + ScheduleModule.forRoot(), + ], + controllers: [DailyChallengesController], + providers: [DailyChallengesService, ChallengeRotationCron], + exports: [DailyChallengesService], +}) +export class DailyChallengesModule {} diff --git a/src/daily-challenges/entities/daily-challenge-completion.entity.ts b/src/daily-challenges/entities/daily-challenge-completion.entity.ts new file mode 100644 index 0000000..f528a7c --- /dev/null +++ b/src/daily-challenges/entities/daily-challenge-completion.entity.ts @@ -0,0 +1,48 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + Index, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { User } from '../../users/entities/user.entity'; +import { DailyChallenge } from './daily-challenge.entity'; + +@Entity('daily_challenge_completions') +@Index(['userId', 'dailyChallengeId'], { unique: true }) +export class DailyChallengeCompletion { + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'userId' }) + user: User; + + @Column() + @Index() + userId: string; + + @ManyToOne(() => DailyChallenge, (challenge) => challenge.completions, { + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'dailyChallengeId' }) + dailyChallenge: DailyChallenge; + + @Column() + @Index() + dailyChallengeId: string; + + @Column({ type: 'int', default: 0 }) + score: number; + + @Column({ type: 'int', default: 0 }) + timeSpent: number; // in seconds + + @Column({ type: 'int', default: 0 }) + streakBonusAwarded: number; + + @CreateDateColumn({ type: 'timestamp with time zone' }) + completedAt: Date; +} diff --git a/src/daily-challenges/entities/daily-challenge.entity.ts b/src/daily-challenges/entities/daily-challenge.entity.ts new file mode 100644 index 0000000..0ed9b05 --- /dev/null +++ b/src/daily-challenges/entities/daily-challenge.entity.ts @@ -0,0 +1,54 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, + ManyToOne, + JoinColumn, + OneToMany, +} from 'typeorm'; +import { Puzzle } from '../../puzzles/entities/puzzle.entity'; +import { DailyChallengeCompletion } from './daily-challenge-completion.entity'; + +@Entity('daily_challenges') +@Index(['challengeDate'], { unique: true }) +@Index(['isActive']) +export class DailyChallenge { + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => Puzzle, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'puzzleId' }) + puzzle: Puzzle; + + @Column() + @Index() + puzzleId: string; + + @Column({ type: 'date' }) + @Index() + challengeDate: Date; // A fixed date (UTC midnight) + + @Column({ type: 'float', default: 1.0 }) + difficultyModifier: number; + + @Column({ type: 'int', default: 100 }) + baseRewardPoints: number; + + @Column({ default: true }) + isActive: boolean; + + @OneToMany( + () => DailyChallengeCompletion, + (completion) => completion.dailyChallenge, + ) + completions: DailyChallengeCompletion[]; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} diff --git a/src/daily-challenges/services/challenge-rotation.cron.ts b/src/daily-challenges/services/challenge-rotation.cron.ts new file mode 100644 index 0000000..80a04ac --- /dev/null +++ b/src/daily-challenges/services/challenge-rotation.cron.ts @@ -0,0 +1,130 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { DailyChallenge } from '../entities/daily-challenge.entity'; +import { Puzzle } from '../../puzzles/entities/puzzle.entity'; + +@Injectable() +export class ChallengeRotationCron { + private readonly logger = new Logger(ChallengeRotationCron.name); + + constructor( + @InjectRepository(DailyChallenge) + private readonly dailyChallengeRepo: Repository, + @InjectRepository(Puzzle) + private readonly puzzleRepo: Repository, + ) {} + + /** + * Helper to get start of current UTC Day + */ + private getStartOfUTCDay(date: Date = new Date()): Date { + const d = new Date(date); + d.setUTCHours(0, 0, 0, 0); + return d; + } + + @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT, { timeZone: 'UTC' }) + async rotateDailyChallenge() { + this.logger.log('Starting daily challenge rotation cron...'); + + const today = this.getStartOfUTCDay(); + + // Deactivate previous active challenges + await this.dailyChallengeRepo.update( + { challengeDate: today, isActive: true }, + { isActive: false }, + ); // (Should not exist yet for today, but edge case handling) + + // Check if we already have one created + const existing = await this.dailyChallengeRepo.findOne({ + where: { challengeDate: today }, + }); + if (existing) { + if (!existing.isActive) { + existing.isActive = true; + await this.dailyChallengeRepo.save(existing); + } + return; + } + + // Determine target difficulty for today. (e.g. rotating: easy -> medium -> hard -> expert) + const dayOfWeek = new Date().getUTCDay(); // 0 is Sunday, 6 is Saturday + let targetDifficulty: 'easy' | 'medium' | 'hard' | 'expert' = 'medium'; + + if (dayOfWeek === 1 || dayOfWeek === 2) + targetDifficulty = 'easy'; // Mon, Tue + else if (dayOfWeek === 3 || dayOfWeek === 4) + targetDifficulty = 'medium'; // Wed, Thu + else if (dayOfWeek === 5) + targetDifficulty = 'hard'; // Fri + else targetDifficulty = 'expert'; // Sat, Sun + + // Fetch random puzzle of this difficulty that has not been a daily challenge recently + const recentChallenges = await this.dailyChallengeRepo.find({ + order: { challengeDate: 'DESC' }, + take: 30, // Don't reuse puzzles from last 30 days + select: ['puzzleId'], + }); + + const excludedPuzzleIds = recentChallenges.map((rc) => rc.puzzleId); + + const query = this.puzzleRepo + .createQueryBuilder('puzzle') + .where('puzzle.isActive = :isActive', { isActive: true }) + .andWhere('puzzle.difficulty = :difficulty', { + difficulty: targetDifficulty, + }); + + if (excludedPuzzleIds.length > 0) { + query.andWhere('puzzle.id NOT IN (:...excludedIds)', { + excludedIds: excludedPuzzleIds, + }); + } + + // Pick a random one + query.orderBy('RANDOM()').limit(1); + const selectedPuzzle = await query.getOne(); + + if (!selectedPuzzle) { + this.logger.error( + 'Failed to find an active puzzle for the daily challenge! Falling back to any puzzle.', + ); + // Fallback: Just grab any active puzzle + const fallbackPuzzle = await this.puzzleRepo + .createQueryBuilder('puzzle') + .where('puzzle.isActive = :isActive', { isActive: true }) + .orderBy('RANDOM()') + .limit(1) + .getOne(); + + if (fallbackPuzzle) { + await this.createChallengeForPuzzle(fallbackPuzzle, today); + } else { + this.logger.error( + 'No puzzles found in the database. Cannot create daily challenge.', + ); + } + return; + } + + await this.createChallengeForPuzzle(selectedPuzzle, today); + } + + private async createChallengeForPuzzle(puzzle: Puzzle, date: Date) { + const dailyChallenge = this.dailyChallengeRepo.create({ + puzzleId: puzzle.id, + challengeDate: date, + isActive: true, + difficultyModifier: 1.0, // Base modifier + baseRewardPoints: puzzle.basePoints * 2, // Double points for daily challenges! + }); + + await this.dailyChallengeRepo.save(dailyChallenge); + this.logger.log( + `Created new daily challenge for ${date.toISOString()}: Puzzle ID ${puzzle.id}`, + ); + } +} diff --git a/src/daily-challenges/services/daily-challenges.service.spec.ts b/src/daily-challenges/services/daily-challenges.service.spec.ts new file mode 100644 index 0000000..3d332d6 --- /dev/null +++ b/src/daily-challenges/services/daily-challenges.service.spec.ts @@ -0,0 +1,211 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { DailyChallengesService } from './daily-challenges.service'; +import { DailyChallenge } from '../entities/daily-challenge.entity'; +import { DailyChallengeCompletion } from '../entities/daily-challenge-completion.entity'; +import { UserStreak } from '../../users/entities/user-streak.entity'; +import { User } from '../../users/entities/user.entity'; + +describe('DailyChallengesService', () => { + let service: DailyChallengesService; + let userStreakRepo: any; + let completionRepo: any; + let dailyChallengeRepo: any; + let userRepo: any; + + beforeEach(async () => { + userStreakRepo = { + findOne: jest.fn(), + create: jest.fn(), + save: jest.fn(), + }; + completionRepo = { + findOne: jest.fn(), + create: jest.fn(), + save: jest.fn(), + find: jest.fn(), + }; + dailyChallengeRepo = { + findOne: jest.fn(), + }; + userRepo = { + findOne: jest.fn(), + }; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + DailyChallengesService, + { + provide: getRepositoryToken(DailyChallenge), + useValue: dailyChallengeRepo, + }, + { + provide: getRepositoryToken(DailyChallengeCompletion), + useValue: completionRepo, + }, + { provide: getRepositoryToken(UserStreak), useValue: userStreakRepo }, + { provide: getRepositoryToken(User), useValue: userRepo }, + ], + }).compile(); + + service = module.get(DailyChallengesService); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('streak tracking and bonus logic', () => { + it('should create a new streak on first completion', async () => { + const challengeId = 'c1'; + const userId = 'u1'; + dailyChallengeRepo.findOne.mockResolvedValue({ + id: challengeId, + isActive: true, + baseRewardPoints: 100, + }); + completionRepo.findOne.mockResolvedValue(null); + userStreakRepo.findOne.mockResolvedValue(null); + + const mockUser = { id: userId }; + userRepo.findOne.mockResolvedValue(mockUser); + + const newStreak = { + user: mockUser, + currentStreak: 1, + streakStartDate: new Date(), + lastPuzzleCompletedAt: new Date(), + }; + userStreakRepo.create.mockReturnValue(newStreak); + userStreakRepo.save.mockResolvedValue(newStreak); + + const completion = { id: 'comp1', streakBonusAwarded: 0 }; + completionRepo.create.mockReturnValue(completion); + + const result = await service.completeChallenge(userId, challengeId, { + score: 100, + timeSpent: 60, + }); + + expect(result.currentStreak).toBe(1); + expect(result.bonusPointsAwarded).toBe(0); + expect(userStreakRepo.create).toHaveBeenCalled(); + expect(userStreakRepo.save).toHaveBeenCalled(); + }); + + it('should increment streak when completed on consecutive days', async () => { + const challengeId = 'c2'; + const userId = 'u2'; + dailyChallengeRepo.findOne.mockResolvedValue({ + id: challengeId, + isActive: true, + baseRewardPoints: 100, + }); + completionRepo.findOne.mockResolvedValue(null); + + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + yesterday.setUTCHours(0, 0, 0, 0); + + const mockStreak = { + user: { id: userId }, + currentStreak: 3, + lastPuzzleCompletedAt: yesterday, + }; + userStreakRepo.findOne.mockResolvedValue(mockStreak); + userStreakRepo.save.mockResolvedValue(mockStreak); + completionRepo.create.mockReturnValue({}); + + const result = await service.completeChallenge(userId, challengeId, { + score: 100, + timeSpent: 60, + }); + + expect(result.currentStreak).toBe(4); + expect(result.bonusPointsAwarded).toBe(15); // 0.05 * 3 = 0.15 * 100 = 15 + expect(userStreakRepo.save).toHaveBeenCalledWith( + expect.objectContaining({ + currentStreak: 4, + }), + ); + }); + + it('should reset streak when a day is missed without a grace period', async () => { + const challengeId = 'c3'; + const userId = 'u3'; + dailyChallengeRepo.findOne.mockResolvedValue({ + id: challengeId, + isActive: true, + baseRewardPoints: 100, + }); + completionRepo.findOne.mockResolvedValue(null); + + const twoDaysAgo = new Date(); + twoDaysAgo.setDate(twoDaysAgo.getDate() - 2); + twoDaysAgo.setUTCHours(0, 0, 0, 0); + + const mockStreak = { + user: { id: userId }, + currentStreak: 5, + lastPuzzleCompletedAt: twoDaysAgo, + }; + userStreakRepo.findOne.mockResolvedValue(mockStreak); + userStreakRepo.save.mockResolvedValue(mockStreak); + completionRepo.create.mockReturnValue({}); + + const result = await service.completeChallenge(userId, challengeId, { + score: 100, + timeSpent: 60, + }); + + expect(result.currentStreak).toBe(1); + expect(result.bonusPointsAwarded).toBe(0); + expect(userStreakRepo.save).toHaveBeenCalledWith( + expect.objectContaining({ + currentStreak: 1, + }), + ); + }); + + it('should maintain streak when a day is missed but within grace period', async () => { + const challengeId = 'c4'; + const userId = 'u4'; + dailyChallengeRepo.findOne.mockResolvedValue({ + id: challengeId, + isActive: true, + baseRewardPoints: 100, + }); + completionRepo.findOne.mockResolvedValue(null); + + const twoDaysAgo = new Date(); + twoDaysAgo.setDate(twoDaysAgo.getDate() - 2); + twoDaysAgo.setUTCHours(0, 0, 0, 0); + + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + + const mockStreak = { + user: { id: userId }, + currentStreak: 5, + lastPuzzleCompletedAt: twoDaysAgo, + streakRecoveryGracePeriodEnd: tomorrow, + }; + + userStreakRepo.findOne.mockResolvedValue(mockStreak); + userStreakRepo.save.mockResolvedValue(mockStreak); + completionRepo.create.mockReturnValue({}); + + const result = await service.completeChallenge(userId, challengeId, { + score: 100, + timeSpent: 60, + }); + + expect(result.currentStreak).toBe(6); // Grace period saved the day! + expect(userStreakRepo.save).toHaveBeenCalledWith( + expect.objectContaining({ + currentStreak: 6, + }), + ); + }); + }); +}); diff --git a/src/daily-challenges/services/daily-challenges.service.ts b/src/daily-challenges/services/daily-challenges.service.ts new file mode 100644 index 0000000..01335cb --- /dev/null +++ b/src/daily-challenges/services/daily-challenges.service.ts @@ -0,0 +1,209 @@ +import { + Injectable, + NotFoundException, + BadRequestException, + Logger, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, LessThan, MoreThanOrEqual } from 'typeorm'; + +import { DailyChallenge } from '../entities/daily-challenge.entity'; +import { DailyChallengeCompletion } from '../entities/daily-challenge-completion.entity'; +import { UserStreak } from '../../users/entities/user-streak.entity'; +import { User } from '../../users/entities/user.entity'; + +@Injectable() +export class DailyChallengesService { + private readonly logger = new Logger(DailyChallengesService.name); + + constructor( + @InjectRepository(DailyChallenge) + private readonly dailyChallengeRepo: Repository, + @InjectRepository(DailyChallengeCompletion) + private readonly completionRepo: Repository, + @InjectRepository(UserStreak) + private readonly userStreakRepo: Repository, + @InjectRepository(User) + private readonly userRepo: Repository, + ) {} + + /** + * Helper to get the start of the current UTC day. + */ + private getStartOfUTCDay(date: Date = new Date()): Date { + const d = new Date(date); + d.setUTCHours(0, 0, 0, 0); + return d; + } + + /** + * Gets the active challenge for today. + */ + async getActiveChallenge(userId?: string) { + const today = this.getStartOfUTCDay(); + + const challenge = await this.dailyChallengeRepo.findOne({ + where: { challengeDate: today, isActive: true }, + relations: ['puzzle'], + }); + + if (!challenge) { + throw new NotFoundException('No active daily challenge found for today.'); + } + + let completed = false; + if (userId) { + const completion = await this.completionRepo.findOne({ + where: { userId, dailyChallengeId: challenge.id }, + }); + completed = !!completion; + } + + return { challenge, completed }; + } + + /** + * Completes a daily challenge. + */ + async completeChallenge( + userId: string, + challengeId: string, + payload: { score: number; timeSpent: number }, + ) { + const challenge = await this.dailyChallengeRepo.findOne({ + where: { id: challengeId, isActive: true }, + }); + + if (!challenge) { + throw new NotFoundException('Challenge not found or no longer active.'); + } + + // Check if already completed + const existingCompletion = await this.completionRepo.findOne({ + where: { userId, dailyChallengeId: challengeId }, + }); + + if (existingCompletion) { + throw new BadRequestException('Challenge already completed today.'); + } + + // Process streak logic and bonus points + const { newStreak, bonusPoints } = await this.processStreakAndBonus( + userId, + challenge.baseRewardPoints, + ); + + // Record completion + const completion = this.completionRepo.create({ + userId, + dailyChallengeId: challengeId, + score: payload.score, + timeSpent: payload.timeSpent, + streakBonusAwarded: bonusPoints, + completedAt: new Date(), + }); + + await this.completionRepo.save(completion); + + return { + success: true, + currentStreak: newStreak, + bonusPointsAwarded: bonusPoints, + totalPointsEarned: challenge.baseRewardPoints + bonusPoints, + completion, + }; + } + + /** + * Retrieves user's challenge history. + */ + async getHistory(userId: string, limit = 30) { + const completions = await this.completionRepo.find({ + where: { userId }, + relations: ['dailyChallenge', 'dailyChallenge.puzzle'], + order: { completedAt: 'DESC' }, + take: limit, + }); + + return completions; + } + + /** + * Processes the user's streak logic based on dates and grace periods. + */ + private async processStreakAndBonus( + userId: string, + basePoints: number, + ): Promise<{ newStreak: number; bonusPoints: number }> { + let userStreak = await this.userStreakRepo.findOne({ + where: { user: { id: userId } }, + }); + const now = new Date(); + const todayStart = this.getStartOfUTCDay(now); + + if (!userStreak) { + // First time completing any puzzle or daily challenge + const user = await this.userRepo.findOne({ where: { id: userId } }); + if (!user) { + throw new NotFoundException('User not found.'); + } + userStreak = this.userStreakRepo.create({ + user: user, + currentStreak: 1, + streakStartDate: now, + lastPuzzleCompletedAt: now, + }); + } else { + const lastCompletion = userStreak.lastPuzzleCompletedAt + ? new Date(userStreak.lastPuzzleCompletedAt) + : null; + const yesterdayStart = new Date(todayStart); + yesterdayStart.setUTCDate(yesterdayStart.getUTCDate() - 1); + + if (!lastCompletion) { + userStreak.currentStreak = 1; + userStreak.streakStartDate = now; + } else { + const lastCompletionUTCDay = this.getStartOfUTCDay(lastCompletion); + + if (lastCompletionUTCDay.getTime() === todayStart.getTime()) { + // Already completed a puzzle today, streak remains same, but we still update the timestamp. + // Note: Daily challenges themselves are limited to 1 per day per user, but `UserStreak` might track all puzzles. + } else if ( + lastCompletionUTCDay.getTime() === yesterdayStart.getTime() + ) { + // Streak maintained normally + userStreak.currentStreak += 1; + } else { + // Check grace period + if ( + userStreak.streakRecoveryGracePeriodEnd && + new Date(userStreak.streakRecoveryGracePeriodEnd) > now + ) { + // Grace period saved the streak + userStreak.currentStreak += 1; + } else { + // Streak broken + userStreak.currentStreak = 1; + userStreak.streakStartDate = now; + } + } + } + userStreak.lastPuzzleCompletedAt = now; + } + + await this.userStreakRepo.save(userStreak); + + // Calculate Bonus (e.g. 5% extra per consecutive day past day 1, capped at 50%) + const streakMultiplier = Math.min( + (userStreak.currentStreak - 1) * 0.05, + 0.5, + ); + let bonusPoints = 0; + if (streakMultiplier > 0) { + bonusPoints = Math.floor(basePoints * streakMultiplier); + } + + return { newStreak: userStreak.currentStreak, bonusPoints }; + } +} diff --git a/src/users/entities/user.entity.ts b/src/users/entities/user.entity.ts index a4e387a..0d238a2 100644 --- a/src/users/entities/user.entity.ts +++ b/src/users/entities/user.entity.ts @@ -13,6 +13,7 @@ import { UserAchievement } from '../../achievements/entities/user-achievement.en import { GameSession } from '../../game-engine/entities/game-session.entity'; import { UserStreak } from './user-streak.entity'; // Added import import { UserPuzzleCompletion } from './user-puzzle-completion.entity'; // Added import +import { DailyChallengeCompletion } from '../../daily-challenges/entities/daily-challenge-completion.entity'; @Entity('users') @Index(['email'], { unique: true }) @@ -144,9 +145,16 @@ export class User { @OneToOne(() => UserStreak, (streak) => streak.user, { cascade: true }) streak: UserStreak; - @OneToMany(() => UserPuzzleCompletion, (completion) => completion.user, { cascade: true }) + @OneToMany(() => UserPuzzleCompletion, (completion) => completion.user, { + cascade: true, + }) puzzleCompletions: UserPuzzleCompletion[]; @OneToMany('UserCollectionProgress', 'user') collectionProgress: any[]; + + @OneToMany(() => DailyChallengeCompletion, (completion) => completion.user, { + cascade: true, + }) + dailyChallengeCompletions: DailyChallengeCompletion[]; }