From e4eb37c40979b08271a0eec496c9781b791a16b5 Mon Sep 17 00:00:00 2001 From: Assad Isah Date: Sun, 6 Jul 2025 23:24:15 +0100 Subject: [PATCH] feat: enhance puzzle submission and progress tracking functionality --- .../entities/puzzle-submission.entity.ts | 52 +++++-------- src/puzzle/enums/puzzle-type.enum.ts | 1 + .../providers/puzzle-progress.provider.ts | 76 +++++++++++++++++++ src/puzzle/puzzle.service.ts | 5 ++ 4 files changed, 99 insertions(+), 35 deletions(-) create mode 100644 src/puzzle/providers/puzzle-progress.provider.ts diff --git a/src/puzzle/entities/puzzle-submission.entity.ts b/src/puzzle/entities/puzzle-submission.entity.ts index 92be7fe..6974bae 100644 --- a/src/puzzle/entities/puzzle-submission.entity.ts +++ b/src/puzzle/entities/puzzle-submission.entity.ts @@ -4,50 +4,32 @@ import { ManyToOne, Column, CreateDateColumn, - JoinColumn, + Unique, } from 'typeorm'; -import { Puzzle } from './puzzle.entity'; -import { ApiProperty } from '@nestjs/swagger'; import { User } from 'src/users/user.entity'; +import { Puzzle } from 'src/puzzle/entities/puzzle.entity'; -@Entity('puzzle_submission') +@Entity('puzzle_submissions') +@Unique(['user', 'puzzle']) export class PuzzleSubmission { - @PrimaryGeneratedColumn() - @ApiProperty() - id: number; + @PrimaryGeneratedColumn('uuid') + id: string; - @Column({ name: 'puzzle_id' }) - @ApiProperty() - puzzleId: number; + @ManyToOne(() => User, { eager: true }) + user: User; @ManyToOne(() => Puzzle, { eager: true }) - @JoinColumn({ name: 'puzzle_id' }) - @ApiProperty({ type: () => Puzzle }) puzzle: Puzzle; - @Column({ name: 'user_id' }) - @ApiProperty() - userId: string; - - @ManyToOne(() => User, { eager: true }) - @JoinColumn({ name: 'user_id' }) - @ApiProperty({ type: () => User }) - user: User; + @Column({ default: false }) + isCorrect: boolean; - @Column({ type: 'jsonb' }) - @ApiProperty({ - type: 'object', - description: 'Submission data like code or answers', - additionalProperties: true - }) - attemptData: Record; + @Column({ nullable: true }) + selectedAnswer?: string; - @Column() - @ApiProperty({ description: 'Whether the submission passed or not' }) - result: boolean; + @Column({ default: false }) + skipped: boolean; - @CreateDateColumn({ name: 'submitted_at' }) - @ApiProperty({ type: String, format: 'date-time' }) - submittedAt: Date; -} - \ No newline at end of file + @CreateDateColumn() + createdAt: Date; +} \ No newline at end of file diff --git a/src/puzzle/enums/puzzle-type.enum.ts b/src/puzzle/enums/puzzle-type.enum.ts index f65e14d..81d632d 100644 --- a/src/puzzle/enums/puzzle-type.enum.ts +++ b/src/puzzle/enums/puzzle-type.enum.ts @@ -2,4 +2,5 @@ export enum PuzzleType { LOGIC = 'logic', CODING = 'coding', BLOCKCHAIN = 'blockchain', + MATH = 'math', } diff --git a/src/puzzle/providers/puzzle-progress.provider.ts b/src/puzzle/providers/puzzle-progress.provider.ts new file mode 100644 index 0000000..4d19755 --- /dev/null +++ b/src/puzzle/providers/puzzle-progress.provider.ts @@ -0,0 +1,76 @@ +@Injectable() +export class PuzzleProgressProvider { +constructor( + @InjectRepository(PuzzleSubmission) + private submissionRepo: Repository, + + @InjectRepository(Puzzle) + private puzzleRepo: Repository, + + @InjectRepository(User) + private userRepo: Repository, + ) {} + + + async submitPuzzleAnswer(dto: SubmitPuzzleDto): Promise { + const { userId, puzzleId, selectedAnswer, skipped } = dto; + + const user = await this.userRepo.findOne({ where: { id: userId } }); + if (!user) throw new NotFoundException('User not found'); + + const puzzle = await this.puzzleRepo.findOne({ where: { id: puzzleId } }); + if (!puzzle) throw new NotFoundException('Puzzle not found'); + + const existing = await this.submissionRepo.findOne({ + where: { user: { id: userId }, puzzle: { id: puzzleId } }, + }); + + if (existing) { + throw new BadRequestException('Puzzle already submitted'); + } + + const isCorrect = !skipped && selectedAnswer === puzzle.solution; + + const submission = this.submissionRepo.create({ + user, + puzzle, + selectedAnswer, + skipped, + isCorrect, + }); + + return await this.submissionRepo.save(submission); + } + + /** + * Get user's puzzle progress by category + */ + async getProgressByCategory(userId: string): Promise< + Record + > { + const allPuzzles = await this.puzzleRepo.find({ + where: { isPublished: true }, + }); + + const completed = await this.submissionRepo.find({ + where: { user: { id: userId }, isCorrect: true }, + relations: ['puzzle'], + }); + + const progressMap: Record = {}; + + for (const puzzle of allPuzzles) { + const key = puzzle.category; + progressMap[key] = progressMap[key] || { completed: 0, total: 0 }; + progressMap[key].total += 1; + } + + for (const submission of completed) { + const key = submission.puzzle.category; + if (progressMap[key]) { + progressMap[key].completed += 1; + } + } + + return progressMap; + }} \ No newline at end of file diff --git a/src/puzzle/puzzle.service.ts b/src/puzzle/puzzle.service.ts index a4c05af..63a12fd 100644 --- a/src/puzzle/puzzle.service.ts +++ b/src/puzzle/puzzle.service.ts @@ -9,6 +9,7 @@ import { User } from '../users/user.entity'; import { SubmitPuzzleDto } from './dto/puzzle.dto'; import { PuzzleSubmissionDto } from '../gamification/dto/puzzle-submission.dto'; import { PuzzleType } from './enums/puzzle-type.enum'; +import { PuzzleProgressProvider } from './providers/puzzle-progress.provider'; @Injectable() export class PuzzleService { @@ -24,6 +25,8 @@ export class PuzzleService { @InjectRepository(User) private readonly userRepository: Repository, private readonly eventEmitter: EventEmitter2, + private readonly puzzleProgressProvider: PuzzleProgressProvider, + ) {} async submitPuzzleSolution( @@ -196,4 +199,6 @@ export class PuzzleService { where: { userId }, }); } + + }