Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { AppService } from './app.service';
import { AppController } from './app.controller';
import { GamificationModule } from './gamification/gamification.module';
import { AchievementModule } from './achievement/achievement.module';
import { PuzzleProgressModule } from './puzzle-progress/puzzle-progress.module';

// const ENV = process.env.NODE_ENV;
// console.log('NODE_ENV:', process.env.NODE_ENV);
Expand Down Expand Up @@ -56,7 +57,8 @@ import { AchievementModule } from './achievement/achievement.module';
IQAssessmentModule,
PuzzleModule,
GamificationModule,
AchievementModule
AchievementModule,
PuzzleProgressModule
],
controllers: [AppController],
providers: [AppService],
Expand Down
1 change: 1 addition & 0 deletions src/puzzle-progress/dto/create-puzzle-progress.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class CreatePuzzleProgressDto {}
4 changes: 4 additions & 0 deletions src/puzzle-progress/dto/update-puzzle-progress.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreatePuzzleProgressDto } from './create-puzzle-progress.dto';

export class UpdatePuzzleProgressDto extends PartialType(CreatePuzzleProgressDto) {}
1 change: 1 addition & 0 deletions src/puzzle-progress/entities/puzzle-progress.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class PuzzleProgress {}
20 changes: 20 additions & 0 deletions src/puzzle-progress/puzzle-progress.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PuzzleProgressController } from './puzzle-progress.controller';
import { PuzzleProgressService } from './puzzle-progress.service';

describe('PuzzleProgressController', () => {
let controller: PuzzleProgressController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [PuzzleProgressController],
providers: [PuzzleProgressService],
}).compile();

controller = module.get<PuzzleProgressController>(PuzzleProgressController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
30 changes: 30 additions & 0 deletions src/puzzle-progress/puzzle-progress.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Controller, Get, Post, Param, Body, HttpCode, HttpStatus, Logger } from '@nestjs/common';
import { PuzzleProgressService } => './puzzle-progress.service';
import { IsString, IsNotEmpty } from 'class-validator';

class SolvePuzzleDto {
@IsNotEmpty()
@IsString()
puzzleId: string;
}

@Controller('users')
export class PuzzleProgressController {
private readonly logger = new Logger(PuzzleProgressController.name);

constructor(private readonly puzzleProgressService: PuzzleProgressService) {}

@Get(':id/progress')
@HttpCode(HttpStatus.OK)
getPuzzleProgress(@Param('id') userId: string): { [key: string]: { completed: number; total: number } } {
this.logger.log(`Received request for puzzle progress for user: ${userId}`);
return this.puzzleProgressService.getPuzzleProgress(userId);
}

@Post(':id/solve-puzzle')
@HttpCode(HttpStatus.NO_CONTENT)
recordPuzzleSolve(@Param('id') userId: string, @Body() solvePuzzleDto: SolvePuzzleDto): void {
this.logger.log(`Received request to record puzzle solve for user: ${userId}, puzzle: ${solvePuzzleDto.puzzleId}`);
this.puzzleProgressService.recordPuzzleSolve(userId, solvePuzzleDto.puzzleId);
}
}
10 changes: 10 additions & 0 deletions src/puzzle-progress/puzzle-progress.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { PuzzleProgressService } from './puzzle-progress.service';
import { PuzzleProgressController } from './puzzle-progress.controller';

@Module({
providers: [PuzzleProgressService],
controllers: [PuzzleProgressController],
exports: [PuzzleProgressService],
})
export class PuzzleProgressModule {}
101 changes: 101 additions & 0 deletions src/puzzle-progress/puzzle-progress.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PuzzleCategory, Puzzle } from './puzzle.entity';
import { NotFoundException } from '@nestjs/common';

describe('PuzzleProgressService (Unit Tests)', () => {
let service: PuzzleProgressService;
let mockPuzzles: Puzzle[];

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [PuzzleProgressService],
}).compile();

service = module.get<PuzzleProgressService>(PuzzleProgressService);
mockPuzzles = service.getAllPuzzles();
});

it('should be defined', () => {
expect(service).toBeDefined();
});

describe('recordPuzzleSolve', () => {
it('should record a puzzle solve for a new user and category', () => {
const userId = 'user1';
const puzzleId = mockPuzzles.find(p => p.category === PuzzleCategory.LOGIC && p.isPublished).id;
service.recordPuzzleSolve(userId, puzzleId);

const progress = service.getPuzzleProgress(userId);
expect(progress[PuzzleCategory.LOGIC].completed).toBe(1);
});

it('should increment completed count for an existing user and category', () => {
const userId = 'user2';
const puzzleId1 = mockPuzzles.filter(p => p.category === PuzzleCategory.CODING && p.isPublished)[0].id;
const puzzleId2 = mockPuzzles.filter(p => p.category === PuzzleCategory.CODING && p.isPublished)[1].id;

service.recordPuzzleSolve(userId, puzzleId1);
service.recordPuzzleSolve(userId, puzzleId2);

const progress = service.getPuzzleProgress(userId);
expect(progress[PuzzleCategory.CODING].completed).toBe(2);
});

it('should throw NotFoundException if puzzle is not found', () => {
const userId = 'user3';
const nonExistentPuzzleId = 'non-existent-puzzle';
expect(() => service.recordPuzzleSolve(userId, nonExistentPuzzleId)).toThrow(NotFoundException);
expect(() => service.recordPuzzleSolve(userId, nonExistentPuzzleId)).toThrow('Puzzle with ID "non-existent-puzzle" not found or is not published.');
});

it('should throw NotFoundException if puzzle is not published', () => {
const userId = 'user4';
const notPublishedPuzzleId = mockPuzzles.find(p => p.isPublished === false).id;
expect(() => service.recordPuzzleSolve(userId, notPublishedPuzzleId)).toThrow(NotFoundException);
expect(() => service.recordPuzzleSolve(userId, notPublishedPuzzleId)).toThrow(`Puzzle with ID "${notPublishedPuzzleId}" not found or is not published.`);
});
});

describe('getPuzzleProgress', () => {
it('should return initial progress for a user with no completed puzzles', () => {
const userId = 'newUser';
const progress = service.getPuzzleProgress(userId);

Object.values(PuzzleCategory).forEach(category => {
const totalPublishedPuzzlesInCategory = mockPuzzles.filter(p => p.category === category && p.isPublished).length;
expect(progress[category].completed).toBe(0);
expect(progress[category].total).toBe(totalPublishedPuzzlesInCategory);
});
});

it('should return correct progress for a user with some completed puzzles', () => {
const userId = 'userWithProgress';
const logicPuzzleId = mockPuzzles.find(p => p.category === PuzzleCategory.LOGIC && p.isPublished).id;
const codingPuzzleId = mockPuzzles.find(p => p.category === PuzzleCategory.CODING && p.isPublished).id;

service.recordPuzzleSolve(userId, logicPuzzleId);
service.recordPuzzleSolve(userId, codingPuzzleId);
service.recordPuzzleSolve(userId, codingPuzzleId);

const progress = service.getPuzzleProgress(userId);

expect(progress[PuzzleCategory.LOGIC].completed).toBe(1);
expect(progress[PuzzleCategory.CODING].completed).toBe(2);
expect(progress[PuzzleCategory.BLOCKCHAIN].completed).toBe(0);
expect(progress[PuzzleCategory.MATH].completed).toBe(0);
expect(progress[PuzzleCategory.GENERAL].completed).toBe(0);

expect(progress[PuzzleCategory.LOGIC].total).toBe(mockPuzzles.filter(p => p.category === PuzzleCategory.LOGIC && p.isPublished).length);
expect(progress[PuzzleCategory.CODING].total).toBe(mockPuzzles.filter(p => p.category === PuzzleCategory.CODING && p.isPublished).length);
});

it('should handle cases where a category has no published puzzles', () => {
const userId = 'userEmptyCategory';

const progress = service.getPuzzleProgress(userId);

});
});
});


156 changes: 156 additions & 0 deletions src/puzzle-progress/puzzle-progress.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { Puzzle, PuzzleCategory, PuzzleType, PuzzleDifficulty } from './puzzle.entity'; // Import new enums
import { v4 as uuidv4 } from 'uuid';

interface UserCategoryProgress {
completed: number;
}

type UserProgressMap = Map<PuzzleCategory, UserCategoryProgress>;

@Injectable()
export class PuzzleProgressService {
private readonly logger = new Logger(PuzzleProgressService.name);

private puzzles: Puzzle[] = [];
private userProgress: Map<string, UserProgressMap> = new Map();

constructor() {
this.seedPuzzles();
}

private seedPuzzles(): void {
this.logger.log('Seeding mock puzzles...');

const mockPuzzles: Omit<Puzzle, 'id'>[] = [
{
title: 'Easy Sudoku',
description: 'A classic 9x9 sudoku puzzle.',
type: PuzzleType.LOGIC,
difficulty: PuzzleDifficulty.EASY,
solution: '...',
isPublished: true,
category: PuzzleCategory.LOGIC,
},
{
title: 'Hard Sudoku',
description: 'A challenging 9x9 sudoku puzzle.',
type: PuzzleType.LOGIC,
difficulty: PuzzleDifficulty.HARD,
solution: '...',
isPublished: true,
category: PuzzleCategory.LOGIC,
},
{
title: 'FizzBuzz Challenge',
description: 'Implement FizzBuzz in your favorite language.',
type: PuzzleType.CODING,
difficulty: PuzzleDifficulty.EASY,
solution: '...',
isPublished: true,
category: PuzzleCategory.CODING,
},
{
title: 'Blockchain Basics Quiz',
description: 'Test your knowledge on fundamental blockchain concepts.',
type: PuzzleType.TRIVIA,
difficulty: PuzzleDifficulty.MEDIUM,
solution: '...',
isPublished: true,
category: PuzzleCategory.BLOCKCHAIN,
},
{
title: 'NFT Minting Exercise',
description: 'Simulate minting an NFT on a testnet.',
type: PuzzleType.CODING,
difficulty: PuzzleDifficulty.HARD,
solution: '...',
isPublished: true,
category: PuzzleCategory.BLOCKCHAIN,
},
{
title: 'Inactive Logic Puzzle',
description: 'This puzzle is not yet published.',
type: PuzzleType.LOGIC,
difficulty: PuzzleDifficulty.MEDIUM,
solution: '...',
isPublished: false,
category: PuzzleCategory.LOGIC,
},
{
title: 'Math Series Problem',
description: 'Find the next number in the sequence.',
type: PuzzleType.MATH,
difficulty: PuzzleDifficulty.EASY,
solution: '...',
isPublished: true,
category: PuzzleCategory.MATH,
},
{
title: 'General Trivia Round 1',
description: 'A mix of general knowledge questions.',
type: PuzzleType.TRIVIA,
difficulty: PuzzleDifficulty.EASY,
solution: '...',
isPublished: true,
category: PuzzleCategory.GENERAL,
},
];

this.puzzles = mockPuzzles.map(p => ({ ...p, id: uuidv4() }));
this.logger.log(`Seeded ${this.puzzles.length} puzzles.`);
}

recordPuzzleSolve(userId: string, puzzleId: string): void {
this.logger.log(`Recording solve for user ${userId}, puzzle ${puzzleId}`);


const puzzle = this.puzzles.find(p => p.id === puzzleId && p.isPublished);
if (!puzzle) {
this.logger.warn(`Puzzle ${puzzleId} not found or not published.`);
throw new NotFoundException(`Puzzle with ID "${puzzleId}" not found or is not published.`);
}

if (!this.userProgress.has(userId)) {
this.userProgress.set(userId, new Map<PuzzleCategory, UserCategoryProgress>());
}

const userCategoryProgress = this.userProgress.get(userId);
const currentCompleted = userCategoryProgress.get(puzzle.category)?.completed || 0;
userCategoryProgress.set(puzzle.category, { completed: currentCompleted + 1 });

this.logger.log(`User ${userId} completed puzzle ${puzzleId} in category ${puzzle.category}. New count: ${currentCompleted + 1}`);
}

getPuzzleProgress(userId: string): { [key in PuzzleCategory]?: { completed: number; total: number } } {
this.logger.log(`Fetching puzzle progress for user ${userId}`);

const progressBreakdown: { [key in PuzzleCategory]?: { completed: number; total: number } } = {};


Object.values(PuzzleCategory).forEach(category => {
const total = this.puzzles.filter(p => p.category === category && p.isPublished).length;
progressBreakdown[category] = { completed: 0, total: total };
});


const userCategoryProgress = this.userProgress.get(userId);
if (userCategoryProgress) {
userCategoryProgress.forEach((data, category) => {
if (progressBreakdown[category]) {
progressBreakdown[category].completed = data.completed;
} else {

progressBreakdown[category] = { completed: data.completed, total: 0 };
}
});
}

return progressBreakdown;
}


getAllPuzzles(): Puzzle[] {
return this.puzzles;
}
}
23 changes: 22 additions & 1 deletion src/puzzle/entities/puzzle.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,33 @@ import { ApiProperty } from '@nestjs/swagger';
import { PuzzleType } from '../enums/puzzle-type.enum';
import { PuzzleDifficulty } from '../enums/puzzle-difficulty.enum';

export enum PuzzleCategory {
LOGIC = 'logic',
CODING = 'coding',
BLOCKCHAIN = 'blockchain',
MATH = 'math',
GENERAL = 'general',
}



export interface Puzzle {
id: string;
title: string;
description: string;
type: PuzzleType;
difficulty: PuzzleDifficulty;
solution: string;
isPublished: boolean;
category: PuzzleCategory; // Added category field
}


@Entity()
export class Puzzle {
@PrimaryGeneratedColumn()
@ApiProperty({ description: 'Unique identifier for the puzzle' })
id: number;
id: string;

@Column()
@ApiProperty({ description: 'Title of the puzzle' })
Expand Down
6 changes: 4 additions & 2 deletions src/puzzle/enums/puzzle-type.enum.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export enum PuzzleType {
LOGIC = 'logic',
CODING = 'coding',
BLOCKCHAIN = 'blockchain',
TRIVIA = 'trivia',
RIDDLE = 'riddle',
CODING = 'coding',
LOGIC = 'logic',
MATH = 'math',
}
Loading