From 4075fcf5bfdd9550108de6183ee86118be7425b7 Mon Sep 17 00:00:00 2001 From: radmehr Date: Sun, 19 Jul 2026 17:39:43 +0330 Subject: [PATCH] feat: add transactions --- .../http/submissions.post.endpoints.http | 8 +- .../providers/answer-submission.provider.ts | 140 +++++++----------- .../providers/calculate-result.provider.ts | 94 +++++------- 3 files changed, 89 insertions(+), 153 deletions(-) diff --git a/src/modules/submissions/http/submissions.post.endpoints.http b/src/modules/submissions/http/submissions.post.endpoints.http index e230a43..310b10d 100644 --- a/src/modules/submissions/http/submissions.post.endpoints.http +++ b/src/modules/submissions/http/submissions.post.endpoints.http @@ -3,14 +3,14 @@ Authorization: Bearer {{$global.accessToken}} Content-Type: application/json { - "testId": 4 + "testId": 1 } -POST {{baseUrl}}/submissions/10/answer +POST {{baseUrl}}/submissions/2/answer Authorization: Bearer {{$global.accessToken}} Content-Type: application/json { - // "choiceId": 1 - "numericValue": 60 + // "choiceId": 2 + "numericValue": 8 } diff --git a/src/modules/submissions/providers/answer-submission.provider.ts b/src/modules/submissions/providers/answer-submission.provider.ts index a78b051..46850a7 100644 --- a/src/modules/submissions/providers/answer-submission.provider.ts +++ b/src/modules/submissions/providers/answer-submission.provider.ts @@ -1,5 +1,5 @@ import { BadRequestException, HttpStatus, Injectable } from '@nestjs/common'; -import { Repository } from 'typeorm'; +import { DataSource, In, Repository } from 'typeorm'; import { Submission } from '../entities/submission.entity'; import { InjectRepository } from '@nestjs/typeorm'; import { AnswerSubmissionDTO } from '../dtos/answer-submission.dto'; @@ -14,7 +14,6 @@ import { AppResponse } from '@/common/responses'; import { QuestionType } from '@/modules/questions/enums/question-type.enum'; import { NumericMetadata } from '@/modules/questions/interfaces/numeric-metadata.interface'; import { SubmissionAnswer } from '../entities/submission-answer.entity'; -import { ChoicesService } from '@/modules/questions/providers/choices.service'; import { CalculateResultProvider } from './calculate-result.provider'; import { QuestionsService } from '@/modules/questions/providers/questions.service'; import { SubmissionResult } from '../entities/submission-result.entity'; @@ -50,15 +49,15 @@ export class AnswerSubmissionProvider { */ private readonly questionsService: QuestionsService, - /** - * Inject Choices Service - */ - private readonly choicesService: ChoicesService, - /** * Inject CalculateResult Provider */ private readonly calculateResultProvider: CalculateResultProvider, + + /** + * Inject DataSource + */ + private readonly dataSource: DataSource, ) {} public async answer( @@ -109,30 +108,22 @@ export class AnswerSubmissionProvider { ); } - // if (submission.user.id !== user.id) { - // throw new ForbiddenException('auth.errors.accessDenied'); - // } - - // const allQuestions = [...submission.test.questions].sort( - // (a, b) => a.order - b.order, - // ); - // const answeredQuestions = submission.answers.map((el) => el.question); - - // const questionStats = this.getQuestionStats( - // allQuestions, - // answeredQuestions, - // ); - - const currentQuestion: Question | null = - await this.questionsService.findOneForAdmin({ + const [currentQuestion, nextQuestion]: Array = + await this.questionsService.findByOptionsForAdmin({ where: { test: { id: submission.test.id }, - order: submission.currentQuestionOrder, + order: In([ + submission.currentQuestionOrder, + submission.currentQuestionOrder + 1, + ]), }, relations: { choices: true, rangeRules: true, }, + order: { + order: 'ASC', + }, }); if (!currentQuestion) { @@ -174,8 +165,8 @@ export class AnswerSubmissionProvider { currentQuestion.type === QuestionType.MULTI_CHOICE && answerSubmissionDto.choiceId ) { - const choice = await this.choicesService.findOneForAdmin( - answerSubmissionDto.choiceId, + const choice = currentQuestion.choices.find( + (el) => el.id === answerSubmissionDto.choiceId, ); if (!choice) { @@ -189,36 +180,45 @@ export class AnswerSubmissionProvider { submissionAnswer.choice = choice; } else if ( currentQuestion.type === QuestionType.NUMERIC && - answerSubmissionDto.numericValue + answerSubmissionDto.numericValue !== undefined ) { submissionAnswer.numericValue = answerSubmissionDto.numericValue; } + let completedResponse: object | null = null; + try { - await this.submissionAnswerRepository.save(submissionAnswer); + completedResponse = await this.dataSource.transaction(async (manager) => { + await manager.save(submissionAnswer); + + if (!nextQuestion) { + const result = await this.calculateResultProvider.calculate( + submission, + manager, + ); + + return { + isCompleted: true, + currentQuestion: null, + result, + }; + } + + await manager.increment( + Submission, + { id: submission.id }, + 'currentQuestionOrder', + 1, + ); + + return null; + }); } catch (err) { throw new AppTimeoutException(err); } - const nextQuestion: Question | null = - await this.questionsService.findOneForAdmin({ - where: { - test: { id: submission.test.id }, - order: submission.currentQuestionOrder + 1, - }, - relations: { - choices: true, - rangeRules: true, - }, - }); - - if (!nextQuestion) { - const result = await this.calculateResultProvider.calculate(submission); - return new AppResponse({ - isCompleted: true, - currentQuestion: null, - result, - }); + if (completedResponse) { + return new AppResponse(completedResponse); } return new AppResponse({ @@ -229,7 +229,11 @@ export class AnswerSubmissionProvider { content: nextQuestion.content, required: nextQuestion.required, metadata: nextQuestion.metadata, - choices: nextQuestion.choices, + choices: nextQuestion.choices.map((el) => ({ + id: el.id, + text: el.text, + order: el.order, + })), }, }); } @@ -255,46 +259,6 @@ export class AnswerSubmissionProvider { return submission; } - private getQuestionStats( - allQuestions: Array, - answeredQuestions: Array, - ) { - const questionStats: Record<'prev' | 'current' | 'next', Question | null> = - { - prev: null, - current: null, - next: null, - }; - - if (allQuestions.length === 0) { - return questionStats; - } - - if (answeredQuestions.length > 0) { - questionStats.prev = answeredQuestions[answeredQuestions.length - 1]; - } - - if (!questionStats.prev) { - questionStats.current = allQuestions[0]; - } else { - const prevQuestionIdx = allQuestions.findIndex( - (val) => val.id === questionStats.prev?.id, - ); - - questionStats.current = allQuestions[prevQuestionIdx + 1]; - } - - if (questionStats.current) { - const currentQuestionIdx = allQuestions.findIndex( - (val) => val.id === questionStats.current?.id, - ); - - questionStats.next = allQuestions[currentQuestionIdx + 1]; - } - - return questionStats; - } - private checkAnswerCorrectness( question: Question, answerSubmissionDto: AnswerSubmissionDTO, diff --git a/src/modules/submissions/providers/calculate-result.provider.ts b/src/modules/submissions/providers/calculate-result.provider.ts index e8f7102..d0a2e85 100644 --- a/src/modules/submissions/providers/calculate-result.provider.ts +++ b/src/modules/submissions/providers/calculate-result.provider.ts @@ -1,50 +1,27 @@ import { Injectable } from '@nestjs/common'; -import { LessThanOrEqual, MoreThanOrEqual, Repository } from 'typeorm'; +import { EntityManager, LessThanOrEqual, MoreThanOrEqual } from 'typeorm'; import { SubmissionResult } from '../entities/submission-result.entity'; -import { InjectRepository } from '@nestjs/typeorm'; import { Submission } from '../entities/submission.entity'; import { QuestionType } from '@/modules/questions/enums/question-type.enum'; -import { QuestionsService } from '@/modules/questions/providers/questions.service'; import { AppTimeoutException } from '@/common/exceptions/app-timeout.exception'; -import { ResultRangeService } from '@/modules/scoring/providers/result-range.service'; import { SubmissionAnswer } from '../entities/submission-answer.entity'; +import { Question } from '@/modules/questions/entities/question.entity'; +import { ResultRange } from '@/modules/scoring/entities/result-range.entity'; @Injectable() export class CalculateResultProvider { - constructor( - /** - * Inject SubmissionResult Repository - */ - @InjectRepository(SubmissionResult) - private readonly submissionResultRepository: Repository, - - /** - * Inject Questions Service - */ - private readonly questionsService: QuestionsService, - - /** - * Inject ResultRange Service - */ - private readonly resultRangeService: ResultRangeService, - - /** - * Inject SubmissionAnswer Repository - */ - @InjectRepository(SubmissionAnswer) - private readonly submissionAnswerRepository: Repository, - ) {} - - public async calculate(submission: Submission) { + public async calculate(submission: Submission, manager: EntityManager) { let score = 0; let submissionAnswers: Array = []; try { - submissionAnswers = await this.submissionAnswerRepository.find({ + submissionAnswers = await manager.find(SubmissionAnswer, { where: { submission: { id: submission.id } }, relations: { - question: true, + question: { + rangeRules: true, + }, choice: true, }, }); @@ -52,39 +29,33 @@ export class CalculateResultProvider { throw new AppTimeoutException(err); } - await Promise.all( - submissionAnswers.map(async (answer) => { + for (const answer of submissionAnswers) { + if (answer.question.type === QuestionType.MULTI_CHOICE && answer.choice) { + score += answer.question.weight * answer.choice.score; + } else if ( + answer.question.type === QuestionType.NUMERIC && + answer.numericValue !== undefined + ) { if ( - answer.question.type === QuestionType.MULTI_CHOICE && - answer.choice + answer.question.rangeRules && + answer.question.rangeRules.length > 0 ) { - score += answer.question.weight * answer.choice.score; - } else if ( - answer.question.type === QuestionType.NUMERIC && - answer.numericValue !== undefined - ) { - const question = await this.questionsService.findOneByIdForAdmin( - answer.question.id, - ); - - if (question.rangeRules && question.rangeRules.length > 0) { - question.rangeRules.forEach((rule) => { - if ( - answer.numericValue && - answer.numericValue >= rule.min && - answer.numericValue <= rule.max - ) { - score += answer.question.weight * rule.score; - } - }); - } + answer.question.rangeRules.forEach((rule) => { + if ( + answer.numericValue !== undefined && + answer.numericValue >= rule.min && + answer.numericValue <= rule.max + ) { + score += answer.question.weight * rule.score; + } + }); } - }), - ); + } + } let totalScores = 0; - const testQuestions = await this.questionsService.findByOptionsForAdmin({ + const testQuestions = await manager.find(Question, { where: { test: { id: submission.test.id } }, relations: { choices: true, @@ -113,14 +84,15 @@ export class CalculateResultProvider { totalScores += question.weight * maxScore; }); - const resultRanges = await this.resultRangeService.findByOptions({ + const resultRanges = await manager.find(ResultRange, { where: { + test: { id: submission.test.id }, minScore: LessThanOrEqual(score), maxScore: MoreThanOrEqual(score), }, }); - const submissionResult = this.submissionResultRepository.create({ + const submissionResult = manager.create(SubmissionResult, { submission, score, probability: Math.ceil((score / totalScores) * 100), @@ -128,7 +100,7 @@ export class CalculateResultProvider { }); try { - await this.submissionResultRepository.save(submissionResult); + await manager.save(submissionResult); } catch (err) { throw new AppTimeoutException(err); }