Files
heala-backend/src/modules/submissions/providers/answer/answer-guest.provider.ts
T
2026-08-13 09:40:21 +03:30

207 lines
5.9 KiB
TypeScript

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<Question>,
/**
* 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<GuestSubmissionSession>(
`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<GuestSubmissionSession>(
`guest_submission:${id}`,
submission,
3600,
);
return new AppResponse({
isCompleted: true,
currentQuestion: null,
result,
});
}
await this.redisService.set<GuestSubmissionSession>(
`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,
})),
},
});
}
}