feat: change answer submission architecture

This commit is contained in:
2026-07-12 17:55:09 +03:30
parent f399ec4a04
commit 4e4731dbde
11 changed files with 238 additions and 87 deletions
@@ -1,9 +1,4 @@
import {
BadRequestException,
ForbiddenException,
HttpStatus,
Injectable,
} from '@nestjs/common';
import { BadRequestException, HttpStatus, Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { Submission } from '../entities/submission.entity';
import { InjectRepository } from '@nestjs/typeorm';
@@ -21,6 +16,8 @@ import { NumericMetadata } from '@/modules/questions/interfaces/numeric-metadata
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';
@Injectable()
export class AnswerSubmissionProvider {
@@ -42,6 +39,17 @@ export class AnswerSubmissionProvider {
@InjectRepository(SubmissionAnswer)
private readonly submissionAnswerRepository: Repository<SubmissionAnswer>,
/**
* Inject SubmissionResult Repository
*/
@InjectRepository(SubmissionResult)
private readonly submissionResultRepository: Repository<SubmissionResult>,
/**
* Inject Questions Service
*/
private readonly questionsService: QuestionsService,
/**
* Inject Choices Service
*/
@@ -84,35 +92,9 @@ export class AnswerSubmissionProvider {
try {
submission = await this.submissionRepository.findOne({
where: { id: submissionId },
where: { id: submissionId, user: { id: user.id } },
relations: {
user: true,
test: {
questions: {
choices: true,
rangeRules: true,
},
},
answers: {
question: true,
},
result: {
resultRange: true,
},
},
order: {
answers: {
question: {
order: 'ASC',
},
},
test: {
questions: {
choices: {
order: 'ASC',
},
},
},
test: true,
},
});
} catch (err) {
@@ -127,44 +109,69 @@ export class AnswerSubmissionProvider {
);
}
if (submission.user.id !== user.id) {
throw new ForbiddenException('auth.errors.accessDenied');
}
// 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 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 questionStats = this.getQuestionStats(
// allQuestions,
// answeredQuestions,
// );
const currentQuestion: Question | null =
await this.questionsService.findOneForAdmin({
where: {
test: { id: submission.test.id },
order: submission.currentQuestionOrder,
},
relations: {
choices: true,
rangeRules: true,
},
});
if (!currentQuestion) {
let result: SubmissionResult | null;
try {
result = await this.submissionResultRepository.findOne({
where: { submission: { id: submission.id } },
relations: {
resultRange: true,
},
});
} catch (err) {
throw new AppTimeoutException(err);
}
if (!questionStats.current) {
return new AppResponse({
isCompleted: true,
currentQuestion: null,
result: submission.result
result: result
? {
score: submission.result.score,
probability: submission.result.probability,
title: submission.result.resultRange?.title,
description: submission.result.resultRange?.description,
score: result.score,
probability: result.probability,
title: result.resultRange?.title,
description: result.resultRange?.description,
}
: null,
});
}
this.checkAnswerCorrectness(questionStats.current, answerSubmissionDto);
this.checkAnswerCorrectness(currentQuestion, answerSubmissionDto);
const submissionAnswer = this.submissionAnswerRepository.create({
submission,
question: questionStats.current,
question: currentQuestion,
});
if (
questionStats.current.type === QuestionType.MULTI_CHOICE &&
currentQuestion.type === QuestionType.MULTI_CHOICE &&
answerSubmissionDto.choiceId
) {
const choice = await this.choicesService.findOneForAdmin(
@@ -181,7 +188,7 @@ export class AnswerSubmissionProvider {
submissionAnswer.choice = choice;
} else if (
questionStats.current.type === QuestionType.NUMERIC &&
currentQuestion.type === QuestionType.NUMERIC &&
answerSubmissionDto.numericValue
) {
submissionAnswer.numericValue = answerSubmissionDto.numericValue;
@@ -193,7 +200,19 @@ export class AnswerSubmissionProvider {
throw new AppTimeoutException(err);
}
if (!questionStats.next) {
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,
@@ -202,14 +221,9 @@ export class AnswerSubmissionProvider {
});
}
const nextQuestion = questionStats.next;
return new AppResponse({
isCompleted: false,
isFinal:
nextQuestion.id === allQuestions[allQuestions.length - 1].id
? true
: undefined,
// isFinal: ,
currentQuestion: {
type: nextQuestion.type,
content: nextQuestion.content,
@@ -7,6 +7,7 @@ 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';
@Injectable()
export class CalculateResultProvider {
@@ -26,13 +27,33 @@ export class CalculateResultProvider {
* Inject ResultRange Service
*/
private readonly resultRangeService: ResultRangeService,
/**
* Inject SubmissionAnswer Repository
*/
@InjectRepository(SubmissionAnswer)
private readonly submissionAnswerRepository: Repository<SubmissionAnswer>,
) {}
public async calculate(submission: Submission) {
let score = 0;
let submissionAnswers: Array<SubmissionAnswer> = [];
try {
submissionAnswers = await this.submissionAnswerRepository.find({
where: { submission: { id: submission.id } },
relations: {
question: true,
choice: true,
},
});
} catch (err) {
throw new AppTimeoutException(err);
}
await Promise.all(
submission.answers.map(async (answer) => {
submissionAnswers.map(async (answer) => {
if (
answer.question.type === QuestionType.MULTI_CHOICE &&
answer.choice
@@ -42,7 +63,7 @@ export class CalculateResultProvider {
answer.question.type === QuestionType.NUMERIC &&
answer.numericValue !== undefined
) {
const question = await this.questionsService.findOneForAdmin(
const question = await this.questionsService.findOneByIdForAdmin(
answer.question.id,
);
@@ -63,7 +84,15 @@ export class CalculateResultProvider {
let totalScores = 0;
submission.test.questions.forEach((question) => {
const testQuestions = await this.questionsService.findByOptionsForAdmin({
where: { test: { id: submission.test.id } },
relations: {
choices: true,
rangeRules: true,
},
});
testQuestions.forEach((question) => {
const answerScores: Array<number> = [];
if (question.type === QuestionType.MULTI_CHOICE) {
question.choices.forEach((choice) => {