import { ConflictException, HttpStatus, Injectable } from '@nestjs/common'; import { AnswerSubmissionDTO } from '../../dtos/answer-submission.dto'; import { User } from '@/modules/users/entities/user.entity'; import { Submission } from '../../entities/submission.entity'; import { AppException } from '@/common/exceptions/app.exception'; import { AppTimeoutException } from '@/common/exceptions/app-timeout.exception'; import { SubmissionResult } from '../../entities/submission-result.entity'; import { AppResponse } from '@/common/responses'; import { checkAnswerCorrectness } from '../../helpers/check-answer-correctness.helper'; import { QuestionType } from '@/modules/questions/enums/question-type.enum'; import { getNextTransition } from '../../helpers/get-next-transition.helper'; import { DataSource, QueryFailedError, Repository } from 'typeorm'; import { InjectRepository } from '@nestjs/typeorm'; import { SubmissionAnswer } from '../../entities/submission-answer.entity'; import { QuestionTransitionType } from '@/modules/questions/enums/question-transition-type.enum'; import { SubmissionStatus } from '../../enums/submission-status.enum'; import { CalculateResultProvider } from './calculate-result.provider'; import { UNIQUE_SUBMISSION_QUESTION_IDX } from '../../constants'; @Injectable() export class AnswerAuthenticatedProvider { constructor( /** * Inject Submission Repository */ @InjectRepository(Submission) private readonly submissionRepository: Repository, /** * Inject SubmisionAnswer Repository */ @InjectRepository(SubmissionAnswer) private readonly submissionAnswerRepository: Repository, /** * Inject SubmissionResult Repository */ @InjectRepository(SubmissionResult) private readonly submissionResultRepository: Repository, /** * Inject DataSource */ private readonly dataSource: DataSource, /** * Inject CalculateResult Provider */ private readonly calculateResultProvider: CalculateResultProvider, ) {} public async answer( id: string, answerSubmissionDto: AnswerSubmissionDTO, user: User, ) { let submission: Submission | null = null; const submissionId = Number(id); if (Number.isNaN(submissionId)) { throw new AppException( 'validation.wrongFieldFormat', HttpStatus.BAD_REQUEST, { field: '$t(submissions.fields.submissionId)' }, ); } try { submission = await this.submissionRepository.findOne({ where: { id: submissionId, user: { id: user.id } }, relations: { test: true, currentQuestion: { choices: true, outgoingTransitions: { toQuestion: { choices: true, }, }, }, }, }); } catch (err) { throw new AppTimeoutException(err); } if (!submission) { throw new AppException( 'common.errors.entityNotFound', HttpStatus.NOT_FOUND, { entity: '$t(submissions.fields.submission)' }, ); } const currentQuestion = submission.currentQuestion; 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); } return new AppResponse({ isCompleted: true, currentQuestion: null, result: result ? { score: result.score, probability: result.probability, title: result.resultRange?.title, description: result.resultRange?.description, } : null, }); } checkAnswerCorrectness(currentQuestion, answerSubmissionDto); const submissionAnswer = this.submissionAnswerRepository.create({ submission, question: currentQuestion, }); if ( currentQuestion.type === QuestionType.SINGLE_CHOICE && answerSubmissionDto.choiceId !== undefined ) { const choice = currentQuestion.choices.find( (el) => el.id === answerSubmissionDto.choiceId, ); if (!choice) { throw new AppException( 'validation.wrongField', HttpStatus.BAD_REQUEST, { field: '$t(submissions.fields.choiceId)' }, ); } submissionAnswer.choice = choice; } else if ( currentQuestion.type === QuestionType.NUMERIC && answerSubmissionDto.numericValue !== undefined ) { submissionAnswer.numericValue = answerSubmissionDto.numericValue; } const nextTransition = getNextTransition( currentQuestion, answerSubmissionDto, ); const nextQuestion = nextTransition.toQuestion ?? null; let completedResponse: object | null = null; try { completedResponse = await this.dataSource.transaction(async (manager) => { await manager.save(submissionAnswer); if ( nextTransition.type === QuestionTransitionType.END || !nextQuestion ) { await manager.update( Submission, { id: submission.id }, { currentQuestion: null, status: SubmissionStatus.COMPLETED }, ); const result = await this.calculateResultProvider.calculateAuthenticated( submission, manager, ); return { isCompleted: true, currentQuestion: null, result, }; } await manager.update( Submission, { id: submission.id }, { currentQuestion: nextQuestion }, ); return null; }); } catch (err) { if ( err instanceof QueryFailedError && // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access (err as any).constraint === UNIQUE_SUBMISSION_QUESTION_IDX ) { throw new ConflictException( 'submissions.errors.uniqueSubmissionQuestion', ); } throw new AppTimeoutException(err); } if (completedResponse) { return new AppResponse(completedResponse); } return new AppResponse({ isCompleted: false, // isFinal: , currentQuestion: { type: nextQuestion?.type, content: nextQuestion?.content, required: nextQuestion?.required, metadata: nextQuestion?.metadata, choices: nextQuestion?.choices.map((el) => ({ id: el.id, text: el.text, order: el.order, })), }, }); } }