import { HttpStatus, Injectable } from '@nestjs/common'; import { AnswerSubmissionDTO } from '../../dtos/answer-submission.dto'; import { isUUID } from 'class-validator'; import { AppException } from '@/common/exceptions/app.exception'; import { GuestSubmissionSession } from '../../interfaces/guest-submission-session.interface'; import { AppTimeoutException } from '@/common/exceptions/app-timeout.exception'; import { Question } from '@/modules/questions/entities/question.entity'; import { AppResponse } from '@/common/responses'; import { checkAnswerCorrectness } from '../../helpers/check-answer-correctness.helper'; import { GuestSubmissionAnswer } from '../../interfaces/guest-submission-answer.interface'; import { QuestionType } from '@/modules/questions/enums/question-type.enum'; import { getNextTransition } from '../../helpers/get-next-transition.helper'; import { QuestionTransitionType } from '@/modules/questions/enums/question-transition-type.enum'; import { InjectRepository } from '@nestjs/typeorm'; import { RedisService } from '@/common/modules/redis/providers/redis.service'; import { Repository } from 'typeorm'; import { CalculateResultProvider } from './calculate-result.provider'; @Injectable() export class AnswerGuestProvider { constructor( /** * Inject Redis Service */ private readonly redisService: RedisService, /** * Inject Questions Repository */ @InjectRepository(Question) private readonly questionsRepository: Repository, /** * Inject CalculateResult Provider */ private readonly calculateResultProvider: CalculateResultProvider, ) {} public async answer(id: string, answerSubmissionDto: AnswerSubmissionDTO) { if (!isUUID(id)) { throw new AppException( 'validation.wrongFieldFormat', HttpStatus.BAD_REQUEST, { field: '$t(submissions.fields.submissionId)' }, ); } const lockKey = `lock:guest_submission:${id}`; const lockToken = await this.redisService.acquireLock(lockKey, 5); if (!lockToken) { throw new AppException( 'common.errors.processingInProgress', HttpStatus.CONFLICT, ); } try { return this.answerGuest(id, answerSubmissionDto); } finally { await this.redisService.releaseLock(lockKey, lockToken); } } private async answerGuest( id: string, answerSubmissionDto: AnswerSubmissionDTO, ) { let submission: GuestSubmissionSession | null = null; try { submission = await this.redisService.get( `guest_submission:${id}`, ); } catch (err) { throw new AppTimeoutException(err); } if (!submission) { throw new AppException( 'common.errors.entityNotFound', HttpStatus.NOT_FOUND, { entity: '$t(submissions.fields.submission)' }, ); } let currentQuestion: Question | null = null; if (submission.currentQuestionId !== null) { try { currentQuestion = await this.questionsRepository.findOne({ where: { id: submission.currentQuestionId }, relations: { choices: true, outgoingTransitions: { toQuestion: { choices: true, }, }, }, }); } catch (err) { throw new AppTimeoutException(err); } } if (!currentQuestion) { return new AppResponse({ isCompleted: true, currentQuestion: null, result: submission.result ? { score: submission.result.score, probability: submission.result.probability, title: submission.result.title, description: submission.result.description, } : null, }); } checkAnswerCorrectness(currentQuestion, answerSubmissionDto); const newAnswer: GuestSubmissionAnswer = { questionId: currentQuestion.id, }; if ( currentQuestion.type === QuestionType.MULTI_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)' }, ); } newAnswer.choiceId = choice.id; } else if ( currentQuestion.type === QuestionType.NUMERIC && answerSubmissionDto.numericValue !== undefined ) { newAnswer.numericValue = answerSubmissionDto.numericValue; } const nextTransition = getNextTransition( currentQuestion, answerSubmissionDto, ); const nextQuestion = nextTransition.toQuestion ?? null; submission.answers = [...submission.answers, newAnswer]; try { if (nextTransition.type === QuestionTransitionType.END || !nextQuestion) { submission.currentQuestionId = null; const result = await this.calculateResultProvider.calculateGuest(submission); await this.redisService.set( `guest_submission:${id}`, submission, 3600, ); return new AppResponse({ isCompleted: true, currentQuestion: null, result, }); } await this.redisService.set( `guest_submission:${id}`, { ...submission, currentQuestionId: nextQuestion.id }, 3600, ); } catch (err) { throw new AppTimeoutException(err); } 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, })), }, }); } }