From c7b3df9d40aa5409a5864ed885c9c4cd29fa6dc5 Mon Sep 17 00:00:00 2001 From: Musa Khalid <112591148+Mkalbani@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:32:09 +0000 Subject: [PATCH 1/3] Implemented Retrieval & Statistics Endpoints --- .../controllers/progress.controller.ts | 103 ++++++++++++++++++ .../src/progress/dtos/category-stats.dto.ts | 33 ++++++ .../src/progress/dtos/overall-stats.dto.ts | 33 ++++++ .../progress/dtos/paginated-progress.dto.ts | 44 ++++++++ .../src/progress/dtos/progress-history.dto.ts | 57 ++++++++++ backend/src/progress/progress.module.ts | 2 +- .../providers/get-category-stats.provider.ts | 57 ++++++++++ .../providers/get-overall-stats.provider.ts | 49 +++++++++ .../get-progress-history.provider.ts | 28 +++++ package-lock.json | 45 +------- 10 files changed, 410 insertions(+), 41 deletions(-) create mode 100644 backend/src/progress/controllers/progress.controller.ts create mode 100644 backend/src/progress/dtos/category-stats.dto.ts create mode 100644 backend/src/progress/dtos/overall-stats.dto.ts create mode 100644 backend/src/progress/dtos/paginated-progress.dto.ts create mode 100644 backend/src/progress/dtos/progress-history.dto.ts create mode 100644 backend/src/progress/providers/get-category-stats.provider.ts create mode 100644 backend/src/progress/providers/get-overall-stats.provider.ts create mode 100644 backend/src/progress/providers/get-progress-history.provider.ts diff --git a/backend/src/progress/controllers/progress.controller.ts b/backend/src/progress/controllers/progress.controller.ts new file mode 100644 index 0000000..66ec87c --- /dev/null +++ b/backend/src/progress/controllers/progress.controller.ts @@ -0,0 +1,103 @@ +import { + Controller, + Get, + Param, + Query, + UseGuards, + BadRequestException, +} from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'; +import { AuthGuard } from '@nestjs/passport'; +import { paginationQueryDto } from '../../common/pagination/paginationQueryDto'; +import { GetProgressHistoryProvider } from '../providers/get-progress-history.provider'; +import { GetCategoryStatsProvider } from '../providers/get-category-stats.provider'; +import { GetOverallStatsProvider } from '../providers/get-overall-stats.provider'; +import { PaginatedProgressDto } from '../dtos/paginated-progress.dto'; +import { CategoryStatsDto } from '../dtos/category-stats.dto'; +import { OverallStatsDto } from '../dtos/overall-stats.dto'; +import { ActiveUser } from '../../auth/decorators/activeUser.decorator'; +import { ActiveUserData } from '../../auth/interfaces/activeInterface'; + +@Controller('progress') +@ApiTags('Progress') +@UseGuards(AuthGuard('jwt')) +@ApiBearerAuth() +export class ProgressController { + constructor( + private readonly getProgressHistoryProvider: GetProgressHistoryProvider, + private readonly getCategoryStatsProvider: GetCategoryStatsProvider, + private readonly getOverallStatsProvider: GetOverallStatsProvider, + ) {} + + @Get() + @ApiOperation({ + summary: 'Get paginated progress history', + description: + 'Retrieve user answer history ordered by most recent attempts first', + }) + @ApiResponse({ + status: 200, + description: 'Paginated progress history', + type: PaginatedProgressDto, + }) + async getProgressHistory( + @ActiveUser() user: ActiveUserData, + @Query() paginationDto: paginationQueryDto, + ) { + if (!user || !user.sub) { + throw new BadRequestException('User not found'); + } + + const { limit = 10, page = 1 } = paginationDto; + return this.getProgressHistoryProvider.getProgressHistory( + user.sub, + page, + limit, + ); + } + + @Get('stats') + @ApiOperation({ + summary: 'Get overall user statistics', + description: + 'Retrieve total attempts, correct answers, accuracy, points earned, and time spent', + }) + @ApiResponse({ + status: 200, + description: 'Overall user statistics', + type: OverallStatsDto, + }) + async getOverallStats(@ActiveUser() user: ActiveUserData) { + if (!user || !user.sub) { + throw new BadRequestException('User not found'); + } + + return this.getOverallStatsProvider.getOverallStats(user.sub); + } + + @Get('category/:id') + @ApiOperation({ + summary: 'Get category-specific statistics', + description: + 'Retrieve total attempts, correct answers, and accuracy for a specific category', + }) + @ApiResponse({ + status: 200, + description: 'Category statistics', + type: CategoryStatsDto, + }) + async getCategoryStats( + @ActiveUser() user: ActiveUserData, + @Param('id') categoryId: string, + ) { + if (!user || !user.sub) { + throw new BadRequestException('User not found'); + } + + if (!categoryId) { + throw new BadRequestException('Category ID is required'); + } + + return this.getCategoryStatsProvider.getCategoryStats(user.sub, categoryId); + } +} diff --git a/backend/src/progress/dtos/category-stats.dto.ts b/backend/src/progress/dtos/category-stats.dto.ts new file mode 100644 index 0000000..211371f --- /dev/null +++ b/backend/src/progress/dtos/category-stats.dto.ts @@ -0,0 +1,33 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class CategoryStatsDto { + @ApiProperty({ + example: '123e4567-e89b-12d3-a456-426614174000', + description: 'Category ID', + }) + categoryId: string; + + @ApiProperty({ + example: 'Algorithms', + description: 'Category name', + }) + categoryName: string; + + @ApiProperty({ + example: 25, + description: 'Total number of attempts in this category', + }) + totalAttempts: number; + + @ApiProperty({ + example: 20, + description: 'Number of correct answers', + }) + correctAnswers: number; + + @ApiProperty({ + example: 80, + description: 'Accuracy percentage', + }) + accuracy: number; +} diff --git a/backend/src/progress/dtos/overall-stats.dto.ts b/backend/src/progress/dtos/overall-stats.dto.ts new file mode 100644 index 0000000..2b48b4c --- /dev/null +++ b/backend/src/progress/dtos/overall-stats.dto.ts @@ -0,0 +1,33 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class OverallStatsDto { + @ApiProperty({ + example: 150, + description: 'Total number of attempts across all categories', + }) + totalAttempts: number; + + @ApiProperty({ + example: 120, + description: 'Total number of correct answers', + }) + totalCorrect: number; + + @ApiProperty({ + example: 80, + description: 'Overall accuracy percentage', + }) + accuracy: number; + + @ApiProperty({ + example: 1500, + description: 'Total points earned across all puzzles', + }) + totalPointsEarned: number; + + @ApiProperty({ + example: 3600, + description: 'Total time spent on all puzzles in seconds', + }) + totalTimeSpent: number; +} diff --git a/backend/src/progress/dtos/paginated-progress.dto.ts b/backend/src/progress/dtos/paginated-progress.dto.ts new file mode 100644 index 0000000..d78833d --- /dev/null +++ b/backend/src/progress/dtos/paginated-progress.dto.ts @@ -0,0 +1,44 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { ProgressHistoryDto } from './progress-history.dto'; + +export class PaginatedProgressDto { + @ApiProperty({ + type: [ProgressHistoryDto], + description: 'Array of progress history records', + }) + data: ProgressHistoryDto[]; + + @ApiProperty({ + example: { + itemsPerPage: 10, + totalItems: 150, + currentPage: 1, + totalPages: 15, + }, + description: 'Pagination metadata', + }) + meta: { + itemsPerPage: number; + totalItems: number; + currentPage: number; + totalPages: number; + }; + + @ApiProperty({ + example: { + first: 'http://localhost:3000/progress?limit=10&page=1', + last: 'http://localhost:3000/progress?limit=10&page=15', + current: 'http://localhost:3000/progress?limit=10&page=1', + previous: 'http://localhost:3000/progress?limit=10&page=1', + next: 'http://localhost:3000/progress?limit=10&page=2', + }, + description: 'Navigation links for pagination', + }) + links: { + first: string; + last: string; + current: string; + previous: string; + next: string; + }; +} diff --git a/backend/src/progress/dtos/progress-history.dto.ts b/backend/src/progress/dtos/progress-history.dto.ts new file mode 100644 index 0000000..fa52771 --- /dev/null +++ b/backend/src/progress/dtos/progress-history.dto.ts @@ -0,0 +1,57 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class ProgressHistoryDto { + @ApiProperty({ + example: '123e4567-e89b-12d3-a456-426614174000', + description: 'Progress record ID', + }) + id: string; + + @ApiProperty({ + example: '123e4567-e89b-12d3-a456-426614174001', + description: 'Puzzle ID', + }) + puzzleId: string; + + @ApiProperty({ + example: 'What is the time complexity of binary search?', + description: 'The puzzle question', + }) + question: string; + + @ApiProperty({ + example: 'O(log n)', + description: 'User answer', + }) + userAnswer: string; + + @ApiProperty({ + example: true, + description: 'Whether the answer was correct', + }) + isCorrect: boolean; + + @ApiProperty({ + example: 10, + description: 'Points earned for this attempt', + }) + pointsEarned: number; + + @ApiProperty({ + example: 45, + description: 'Time spent on this puzzle in seconds', + }) + timeSpent: number; + + @ApiProperty({ + example: '2024-01-28T10:30:00Z', + description: 'When the puzzle was attempted', + }) + attemptedAt: Date; + + @ApiProperty({ + example: '123e4567-e89b-12d3-a456-426614174002', + description: 'Category ID', + }) + categoryId: string; +} diff --git a/backend/src/progress/progress.module.ts b/backend/src/progress/progress.module.ts index 74202c2..8bf3f54 100644 --- a/backend/src/progress/progress.module.ts +++ b/backend/src/progress/progress.module.ts @@ -15,4 +15,4 @@ import { ProgressCalculationProvider } from './providers/progress-calculation.pr providers: [ProgressService, ProgressCalculationProvider], exports: [ProgressService, ProgressCalculationProvider, TypeOrmModule], }) -export class ProgressModule {} +export class ProgressModule {} \ No newline at end of file diff --git a/backend/src/progress/providers/get-category-stats.provider.ts b/backend/src/progress/providers/get-category-stats.provider.ts new file mode 100644 index 0000000..7d24990 --- /dev/null +++ b/backend/src/progress/providers/get-category-stats.provider.ts @@ -0,0 +1,57 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { UserProgress } from '../entities/progress.entity'; +import { CategoryStatsDto } from '../dtos/category-stats.dto'; + +@Injectable() +export class GetCategoryStatsProvider { + constructor( + @InjectRepository(UserProgress) + private readonly progressRepo: Repository, + ) {} + + async getCategoryStats(userId: string, categoryId: string) { + const result = await this.progressRepo + .createQueryBuilder('progress') + .select('progress.categoryId', 'categoryId') + .addSelect('COUNT(*)', 'totalAttempts') + .addSelect('SUM(CASE WHEN progress.isCorrect = true THEN 1 ELSE 0 END)', 'correctAnswers') + .where('progress.userId = :userId', { userId }) + .andWhere('progress.categoryId = :categoryId', { categoryId }) + .groupBy('progress.categoryId') + .getRawOne(); + + if (!result) { + return { + categoryId, + categoryName: '', + totalAttempts: 0, + correctAnswers: 0, + accuracy: 0, + }; + } + + const totalAttempts = parseInt(result.totalAttempts, 10) || 0; + const correctAnswers = parseInt(result.correctAnswers, 10) || 0; + const accuracy = + totalAttempts > 0 ? Math.round((correctAnswers / totalAttempts) * 100) : 0; + + // Get category name + const category = await this.progressRepo + .createQueryBuilder('progress') + .leftJoinAndSelect('progress.category', 'category') + .where('progress.categoryId = :categoryId', { categoryId }) + .select('category.name') + .limit(1) + .getRawOne(); + + return { + categoryId, + categoryName: category?.category_name || '', + totalAttempts, + correctAnswers, + accuracy, + }; + } +} diff --git a/backend/src/progress/providers/get-overall-stats.provider.ts b/backend/src/progress/providers/get-overall-stats.provider.ts new file mode 100644 index 0000000..447c726 --- /dev/null +++ b/backend/src/progress/providers/get-overall-stats.provider.ts @@ -0,0 +1,49 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { UserProgress } from '../entities/progress.entity'; +import { OverallStatsDto } from '../dtos/overall-stats.dto'; + +@Injectable() +export class GetOverallStatsProvider { + constructor( + @InjectRepository(UserProgress) + private readonly progressRepo: Repository, + ) {} + + async getOverallStats(userId: string): Promise { + const result = await this.progressRepo + .createQueryBuilder('progress') + .select('COUNT(*)', 'totalAttempts') + .addSelect('SUM(CASE WHEN progress.isCorrect = true THEN 1 ELSE 0 END)', 'totalCorrect') + .addSelect('SUM(progress.pointsEarned)', 'totalPointsEarned') + .addSelect('SUM(progress.timeSpent)', 'totalTimeSpent') + .where('progress.userId = :userId', { userId }) + .getRawOne(); + + if (!result) { + return { + totalAttempts: 0, + totalCorrect: 0, + accuracy: 0, + totalPointsEarned: 0, + totalTimeSpent: 0, + }; + } + + const totalAttempts = parseInt(result.totalAttempts, 10) || 0; + const totalCorrect = parseInt(result.totalCorrect, 10) || 0; + const accuracy = + totalAttempts > 0 + ? Math.round((totalCorrect / totalAttempts) * 100) + : 0; + + return { + totalAttempts, + totalCorrect, + accuracy, + totalPointsEarned: parseInt(result.totalPointsEarned, 10) || 0, + totalTimeSpent: parseInt(result.totalTimeSpent, 10) || 0, + }; + } +} diff --git a/backend/src/progress/providers/get-progress-history.provider.ts b/backend/src/progress/providers/get-progress-history.provider.ts new file mode 100644 index 0000000..46b1043 --- /dev/null +++ b/backend/src/progress/providers/get-progress-history.provider.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { UserProgress } from '../entities/progress.entity'; +import { paginate } from '../../common/pagination/paginate'; +import { ProgressHistoryDto } from '../dtos/progress-history.dto'; + +@Injectable() +export class GetProgressHistoryProvider { + constructor( + @InjectRepository(UserProgress) + private readonly progressRepo: Repository, + ) {} + + async getProgressHistory( + userId: string, + page: number = 1, + limit: number = 10, + ) { + const qb = this.progressRepo + .createQueryBuilder('progress') + .leftJoinAndSelect('progress.puzzle', 'puzzle') + .where('progress.userId = :userId', { userId }) + .orderBy('progress.attemptedAt', 'DESC'); + + return paginate(qb, page, limit); + } +} diff --git a/package-lock.json b/package-lock.json index a04b709..3d7e3b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -155,7 +155,6 @@ "integrity": "sha512-Q5FsI3Cw0fGMXhmsg7c08i4EmXCrcl+WnAxb6LYOLHw4JFFC3yzmx9LaXZ7QMbA+JZXbigU2TirI7RAfO0Qlnw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@swc/counter": "^0.1.3", "@xhmikosr/bin-wrapper": "^13.0.5", @@ -202,7 +201,6 @@ "dev": true, "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.24" @@ -241,7 +239,6 @@ "integrity": "sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -350,7 +347,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -394,7 +390,6 @@ "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.26.tgz", "integrity": "sha512-o2RrBNn3lczx1qv4j+JliVMmtkPSqEGpG0UuZkt9tCfWkoXKu8MZnjvp2GjWPll1SehwemQw6xrbVRhmOglj8Q==", "license": "MIT", - "peer": true, "dependencies": { "@sqltools/formatter": "^1.2.5", "ansis": "^3.17.0", @@ -527,7 +522,6 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -842,7 +836,6 @@ "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -3580,7 +3573,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.6.tgz", "integrity": "sha512-krKwLLcFmeuKDqngG2N/RuZHCs2ycsKcxWIDgcm7i1lf3sQ0iG03ci+DsP/r3FcT/eJDFsIHnKtNta2LIi7PzQ==", "license": "MIT", - "peer": true, "dependencies": { "file-type": "21.0.0", "iterare": "1.2.1", @@ -3640,7 +3632,6 @@ "integrity": "sha512-siWX7UDgErisW18VTeJA+x+/tpNZrJewjTBsRPF3JVxuWRuAB1kRoiJcxHgln8Lb5UY9NdvklITR84DUEXD0Cg==", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@nuxt/opencollective": "0.4.1", "fast-safe-stringify": "2.1.1", @@ -3737,7 +3728,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.6.tgz", "integrity": "sha512-HErwPmKnk+loTq8qzu1up+k7FC6Kqa8x6lJ4cDw77KnTxLzsCaPt+jBvOq6UfICmfqcqCCf3dKXg+aObQp+kIQ==", "license": "MIT", - "peer": true, "dependencies": { "cors": "2.8.5", "express": "5.1.0", @@ -4932,7 +4922,6 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -5190,7 +5179,6 @@ "integrity": "sha512-lr3jdBw/BGj49Eps7EvqlUaoeA0xpj3pc0RoJkHpYaCHkVK7i28dKyImLQb3JVlqs3aYSXf7qYuWOW/fgZnTXQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -5332,7 +5320,6 @@ "integrity": "sha512-r1XG74QgShUgXph1BYseJ+KZd17bKQib/yF3SR+demvytiRXrwd12Blnz5eYGm8tXaeRdd4x88MlfwldHoudGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.42.0", "@typescript-eslint/types": "8.42.0", @@ -6328,7 +6315,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "devOptional": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6414,7 +6400,6 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -6863,7 +6848,6 @@ "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", "license": "MIT", - "peer": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", @@ -7288,7 +7272,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001737", "electron-to-chromium": "^1.5.211", @@ -7767,15 +7750,13 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/class-validator": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz", "integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==", "license": "MIT", - "peer": true, "dependencies": { "@types/validator": "^13.11.8", "libphonenumber-js": "^1.11.1", @@ -8984,7 +8965,6 @@ "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -9074,7 +9054,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -9176,7 +9155,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -9651,7 +9629,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -11143,7 +11120,6 @@ "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.7.0.tgz", "integrity": "sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g==", "license": "MIT", - "peer": true, "dependencies": { "@ioredis/commands": "^1.3.0", "cluster-key-slot": "^1.1.0", @@ -11812,7 +11788,6 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -14690,7 +14665,6 @@ "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", "license": "MIT", - "peer": true, "dependencies": { "passport-strategy": "1.x.x", "pause": "0.0.1", @@ -14866,7 +14840,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -15193,7 +15166,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -15466,7 +15438,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -15476,7 +15447,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -15496,7 +15466,6 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -15568,8 +15537,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -15584,8 +15552,7 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", @@ -15922,7 +15889,6 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -16040,7 +16006,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -17668,7 +17633,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -18064,7 +18028,6 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -18565,6 +18528,7 @@ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -18579,6 +18543,7 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=4.0" } From 99bf1e1fccbefd22a25e765aced5df0be153f7ff Mon Sep 17 00:00:00 2001 From: Musa Khalid <112591148+Mkalbani@users.noreply.github.com> Date: Wed, 28 Jan 2026 19:19:28 +0000 Subject: [PATCH 2/3] Implemented Daily Quest Logic --- .../controllers/daily-quest.controller.ts | 40 ++++- .../quests/dtos/complete-daily-quest.dto.ts | 57 +++++++ .../complete-daily-quest.provider.ts | 161 ++++++++++++++++++ .../quests/providers/daily-quest.service.ts | 16 +- backend/src/quests/quests.module.ts | 6 +- .../providers/update-streak.provider.ts | 101 +++++++++++ backend/src/streak/strerak.module.ts | 6 +- 7 files changed, 381 insertions(+), 6 deletions(-) create mode 100644 backend/src/quests/dtos/complete-daily-quest.dto.ts create mode 100644 backend/src/quests/providers/complete-daily-quest.provider.ts create mode 100644 backend/src/streak/providers/update-streak.provider.ts diff --git a/backend/src/quests/controllers/daily-quest.controller.ts b/backend/src/quests/controllers/daily-quest.controller.ts index 359db93..1551839 100644 --- a/backend/src/quests/controllers/daily-quest.controller.ts +++ b/backend/src/quests/controllers/daily-quest.controller.ts @@ -1,6 +1,7 @@ import { Controller, Get, + Post, HttpCode, HttpStatus, UnauthorizedException, @@ -9,6 +10,7 @@ import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { DailyQuestService } from '../providers/daily-quest.service'; import { DailyQuestResponseDto } from '../dtos/daily-quest-response.dto'; import { DailyQuestStatusDto } from '../dtos/daily-quest-status.dto'; +import { CompleteDailyQuestResponseDto } from '../dtos/complete-daily-quest.dto'; import { ActiveUser } from '../../auth/decorators/activeUser.decorator'; import { Auth } from '../../auth/decorators/auth.decorator'; import { authType } from '../../auth/enum/auth-type.enum'; @@ -40,7 +42,7 @@ export class DailyQuestController { async getTodaysDailyQuest( @ActiveUser('sub') userId: string, ): Promise { - console.log('REQUEST_USER_KEY:', request['user']); // Common key + console.log('REQUEST_USER_KEY:', request['user']); console.log('Full request keys:', Object.keys(request)); console.log('userId:', userId); console.log('fullUser:', User); @@ -76,4 +78,38 @@ export class DailyQuestController { } return this.dailyQuestService.getTodaysDailyQuestStatus(userId); } -} + + @Post('complete') + @Auth(authType.Bearer) + @HttpCode(HttpStatus.OK) + @ApiOperation({ + summary: 'Complete daily quest and award bonus', + description: + 'Finalizes the daily quest, awards bonus XP, and updates user streak. This endpoint is idempotent - repeated calls will not duplicate rewards.', + }) + @ApiResponse({ + status: 200, + description: 'Daily quest completed successfully', + type: CompleteDailyQuestResponseDto, + }) + @ApiResponse({ + status: 400, + description: 'Quest not fully completed or validation failed', + }) + @ApiResponse({ + status: 404, + description: 'No daily quest found for today', + }) + @ApiResponse({ + status: 401, + description: 'Unauthorized - valid authentication required', + }) + async completeDailyQuest( + @ActiveUser('sub') userId: string, + ): Promise { + if (!userId) { + throw new UnauthorizedException('User ID not found in token'); + } + return this.dailyQuestService.completeDailyQuest(userId); + } +} \ No newline at end of file diff --git a/backend/src/quests/dtos/complete-daily-quest.dto.ts b/backend/src/quests/dtos/complete-daily-quest.dto.ts new file mode 100644 index 0000000..e5af639 --- /dev/null +++ b/backend/src/quests/dtos/complete-daily-quest.dto.ts @@ -0,0 +1,57 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class CompleteDailyQuestResponseDto { + @ApiProperty({ + description: 'Whether the quest was completed successfully', + example: true, + }) + success: boolean; + + @ApiProperty({ + description: 'Status message', + example: 'Daily quest completed successfully!', + }) + message: string; + + @ApiProperty({ + description: 'Bonus XP awarded for completion', + example: 100, + }) + bonusXp: number; + + @ApiProperty({ + description: 'Total XP earned in this quest (including bonus)', + example: 850, + }) + totalXp: number; + + @ApiProperty({ + description: 'Updated user streak information', + type: 'object', + properties: { + currentStreak: { + type: 'number', + example: 5, + }, + longestStreak: { + type: 'number', + example: 10, + }, + lastActivityDate: { + type: 'string', + example: '2026-01-28', + }, + }, + }) + streakInfo: { + currentStreak: number; + longestStreak: number; + lastActivityDate: string; + }; + + @ApiProperty({ + description: 'When the quest was completed', + example: '2026-01-28T15:30:00Z', + }) + completedAt: Date; +} \ No newline at end of file diff --git a/backend/src/quests/providers/complete-daily-quest.provider.ts b/backend/src/quests/providers/complete-daily-quest.provider.ts new file mode 100644 index 0000000..9321d71 --- /dev/null +++ b/backend/src/quests/providers/complete-daily-quest.provider.ts @@ -0,0 +1,161 @@ +// backend/src/quests/providers/complete-daily-quest.provider.ts +import { + Injectable, + Logger, + BadRequestException, + NotFoundException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, DataSource } from 'typeorm'; +import { DailyQuest } from '../entities/daily-quest.entity'; +import { User } from '../../users/user.entity'; +import { UpdateStreakProvider } from '../../streak/providers/update-streak.provider'; +import { CompleteDailyQuestResponseDto } from '../dtos/complete-daily-quest.dto'; + +@Injectable() +export class CompleteDailyQuestProvider { + private readonly logger = new Logger(CompleteDailyQuestProvider.name); + private readonly BONUS_XP = 100; + + constructor( + @InjectRepository(DailyQuest) + private readonly dailyQuestRepository: Repository, + @InjectRepository(User) + private readonly userRepository: Repository, + private readonly updateStreakProvider: UpdateStreakProvider, + private readonly dataSource: DataSource, + ) {} + + async execute(userId: string): Promise { + const todayDate = this.getTodayDateString(); + this.logger.log( + `Attempting to complete daily quest for user ${userId} on ${todayDate}`, + ); + + // Parse userId to number for streak operations + const userIdNumber = parseInt(userId, 10); + if (isNaN(userIdNumber)) { + throw new BadRequestException('Invalid user ID format'); + } + + // Use transaction to ensure atomicity + const transactionResult = await this.dataSource.transaction( + async (manager) => { + // 1. Find today's quest + const quest = await manager.findOne(DailyQuest, { + where: { userId: userId, questDate: todayDate }, + lock: { mode: 'pessimistic_write' }, // Lock for update + }); + + if (!quest) { + throw new NotFoundException( + `No daily quest found for user ${userId} on ${todayDate}`, + ); + } + + // 2. Validate completion criteria + if (quest.completedQuestions !== quest.totalQuestions) { + throw new BadRequestException( + `Quest not fully completed. Progress: ${quest.completedQuestions}/${quest.totalQuestions}`, + ); + } + + // 3. Check idempotency - already completed + if (quest.isCompleted && quest.completedAt) { + this.logger.log( + `Quest already completed for user ${userId}, returning cached result`, + ); + + // Return existing completion data + const streak = await this.updateStreakProvider.getStreak( + userIdNumber, + ); + return { + isAlreadyCompleted: true, + success: true, + message: 'Daily quest already completed', + bonusXp: this.BONUS_XP, + totalXp: quest.pointsEarned, + streakInfo: { + currentStreak: streak?.currentStreak || 0, + longestStreak: streak?.longestStreak || 0, + lastActivityDate: streak?.lastActivityDate || todayDate, + }, + completedAt: quest.completedAt, + }; + } + + // 4. Mark quest as completed + quest.isCompleted = true; + quest.completedAt = new Date(); + quest.pointsEarned += this.BONUS_XP; // Add bonus XP + await manager.save(DailyQuest, quest); + + this.logger.log( + `Quest marked complete for user ${userId}, awarded ${this.BONUS_XP} bonus XP`, + ); + + // 5. Update user XP + const user = await manager.findOne(User, { + where: { id: userId }, + lock: { mode: 'pessimistic_write' }, + }); + + if (!user) { + throw new NotFoundException(`User ${userId} not found`); + } + + user.xp += this.BONUS_XP; + // Simple level calculation: level = floor(xp / 100) + 1 + user.level = Math.floor(user.xp / 100) + 1; + await manager.save(User, user); + + this.logger.log( + `Updated user ${userId} XP to ${user.xp}, level ${user.level}`, + ); + + // 6. Return quest data for post-transaction processing + return { + isAlreadyCompleted: false, + userId: userIdNumber, + completedAt: quest.completedAt, + totalXp: quest.pointsEarned, + }; + }, + ); + + // Handle already completed case + if (transactionResult.isAlreadyCompleted) { + return transactionResult as CompleteDailyQuestResponseDto; + } + + // 7. Update streak after transaction commits + const streak = await this.updateStreakProvider.updateStreak( + transactionResult.userId, + ); + + this.logger.log( + `Updated streak for user ${transactionResult.userId}: ${streak.currentStreak} days`, + ); + + // 8. Build response + return { + success: true, + message: 'Daily quest completed successfully!', + bonusXp: this.BONUS_XP, + totalXp: transactionResult.totalXp, + streakInfo: { + currentStreak: streak.currentStreak, + longestStreak: streak.longestStreak, + lastActivityDate: + streak.lastActivityDate || this.getTodayDateString(), + }, + completedAt: transactionResult.completedAt, + }; + } + + private getTodayDateString(): string { + const now = new Date(); + return now.toISOString().split('T')[0]; + } +} \ No newline at end of file diff --git a/backend/src/quests/providers/daily-quest.service.ts b/backend/src/quests/providers/daily-quest.service.ts index a37c6b8..3da8845 100644 --- a/backend/src/quests/providers/daily-quest.service.ts +++ b/backend/src/quests/providers/daily-quest.service.ts @@ -1,14 +1,18 @@ +// backend/src/quests/providers/daily-quest.service.ts import { Injectable } from '@nestjs/common'; import { DailyQuestResponseDto } from '../dtos/daily-quest-response.dto'; import { DailyQuestStatusDto } from '../dtos/daily-quest-status.dto'; +import { CompleteDailyQuestResponseDto } from '../dtos/complete-daily-quest.dto'; import { GetTodaysDailyQuestProvider } from './getTodaysDailyQuest.provider'; import { GetTodaysDailyQuestStatusProvider } from './getTodaysDailyQuestStatus.provider'; +import { CompleteDailyQuestProvider } from './complete-daily-quest.provider'; @Injectable() export class DailyQuestService { constructor( private readonly getTodaysDailyQuestProvider: GetTodaysDailyQuestProvider, private readonly getTodaysDailyQuestStatusProvider: GetTodaysDailyQuestStatusProvider, + private readonly completeDailyQuestProvider: CompleteDailyQuestProvider, ) {} async getTodaysDailyQuest(userId: string): Promise { @@ -23,4 +27,14 @@ export class DailyQuestService { ): Promise { return this.getTodaysDailyQuestStatusProvider.execute(userId); } -} + + /** + * Completes today's Daily Quest and awards bonus XP + * This method is idempotent - repeated calls will not duplicate rewards + */ + async completeDailyQuest( + userId: string, + ): Promise { + return this.completeDailyQuestProvider.execute(userId); + } +} \ No newline at end of file diff --git a/backend/src/quests/quests.module.ts b/backend/src/quests/quests.module.ts index 8572299..810f939 100644 --- a/backend/src/quests/quests.module.ts +++ b/backend/src/quests/quests.module.ts @@ -6,9 +6,11 @@ import { DailyQuestController } from './controllers/daily-quest.controller'; import { DailyQuestService } from './providers/daily-quest.service'; import { GetTodaysDailyQuestProvider } from './providers/getTodaysDailyQuest.provider'; import { GetTodaysDailyQuestStatusProvider } from './providers/getTodaysDailyQuestStatus.provider'; +import { CompleteDailyQuestProvider } from './providers/complete-daily-quest.provider'; import { PuzzlesModule } from '../puzzles/puzzles.module'; import { ProgressModule } from '../progress/progress.module'; import { UsersModule } from '../users/users.module'; +import { StreakModule } from '../streak/strerak.module'; @Module({ imports: [ @@ -16,13 +18,15 @@ import { UsersModule } from '../users/users.module'; PuzzlesModule, ProgressModule, UsersModule, + StreakModule, ], controllers: [DailyQuestController], providers: [ DailyQuestService, GetTodaysDailyQuestProvider, GetTodaysDailyQuestStatusProvider, + CompleteDailyQuestProvider, ], exports: [TypeOrmModule, DailyQuestService], }) -export class QuestsModule {} +export class QuestsModule {} \ No newline at end of file diff --git a/backend/src/streak/providers/update-streak.provider.ts b/backend/src/streak/providers/update-streak.provider.ts new file mode 100644 index 0000000..4f54b18 --- /dev/null +++ b/backend/src/streak/providers/update-streak.provider.ts @@ -0,0 +1,101 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Streak } from '../entities/streak.entity'; + +@Injectable() +export class UpdateStreakProvider { + private readonly logger = new Logger(UpdateStreakProvider.name); + + constructor( + @InjectRepository(Streak) + private readonly streakRepository: Repository, + ) {} + + /** + * Updates user streak based on daily quest completion + * Handles streak continuation, breaks, and new streaks + */ + async updateStreak(userId: number): Promise { + const todayDate = this.getTodayDateString(); + this.logger.log(`Updating streak for user ${userId} on ${todayDate}`); + + // Find or create user's streak record + let streak = await this.streakRepository.findOne({ + where: { userId }, + }); + + if (!streak) { + // First time user - create new streak + streak = this.streakRepository.create({ + userId, + currentStreak: 1, + longestStreak: 1, + lastActivityDate: todayDate, + streakDates: [todayDate], + }); + this.logger.log(`Created new streak for user ${userId}`); + } else { + // Check if already updated today (idempotency) + if (streak.lastActivityDate === todayDate) { + this.logger.log( + `Streak already updated today for user ${userId}, returning existing`, + ); + return streak; + } + + // Calculate streak continuation + const yesterday = this.getYesterdayDateString(); + const isConsecutive = streak.lastActivityDate === yesterday; + + if (isConsecutive) { + // Continue streak + streak.currentStreak += 1; + this.logger.log( + `Continued streak for user ${userId}: ${streak.currentStreak} days`, + ); + } else { + // Streak broken - start new streak + streak.currentStreak = 1; + this.logger.log(`Streak broken for user ${userId}, starting fresh`); + } + + // Update longest streak if needed + if (streak.currentStreak > streak.longestStreak) { + streak.longestStreak = streak.currentStreak; + this.logger.log( + `New longest streak for user ${userId}: ${streak.longestStreak}`, + ); + } + + // Update last activity date and add to streak dates + streak.lastActivityDate = todayDate; + if (!streak.streakDates.includes(todayDate)) { + streak.streakDates.push(todayDate); + } + } + + // Save and return + return await this.streakRepository.save(streak); + } + + /** + * Get user's current streak information + */ + async getStreak(userId: number): Promise { + return await this.streakRepository.findOne({ + where: { userId }, + }); + } + + private getTodayDateString(): string { + const now = new Date(); + return now.toISOString().split('T')[0]; + } + + private getYesterdayDateString(): string { + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + return yesterday.toISOString().split('T')[0]; + } +} \ No newline at end of file diff --git a/backend/src/streak/strerak.module.ts b/backend/src/streak/strerak.module.ts index 6682d6f..18876b1 100644 --- a/backend/src/streak/strerak.module.ts +++ b/backend/src/streak/strerak.module.ts @@ -1,9 +1,11 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Streak } from './entities/streak.entity'; +import { UpdateStreakProvider } from './providers/update-streak.provider'; @Module({ imports: [TypeOrmModule.forFeature([Streak])], - exports: [TypeOrmModule], + providers: [UpdateStreakProvider], + exports: [TypeOrmModule, UpdateStreakProvider], }) -export class StreakModule {} +export class StreakModule {} \ No newline at end of file From b203c837352120dbb796b376e01acc41b4be0135 Mon Sep 17 00:00:00 2001 From: Musa Khalid <112591148+Mkalbani@users.noreply.github.com> Date: Thu, 29 Jan 2026 19:48:46 +0000 Subject: [PATCH 3/3] fixed undefined error --- backend/src/quests/providers/complete-daily-quest.provider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/quests/providers/complete-daily-quest.provider.ts b/backend/src/quests/providers/complete-daily-quest.provider.ts index 9321d71..d8c1997 100644 --- a/backend/src/quests/providers/complete-daily-quest.provider.ts +++ b/backend/src/quests/providers/complete-daily-quest.provider.ts @@ -131,7 +131,7 @@ export class CompleteDailyQuestProvider { // 7. Update streak after transaction commits const streak = await this.updateStreakProvider.updateStreak( - transactionResult.userId, + transactionResult.userId!, ); this.logger.log(