207 lines
5.9 KiB
TypeScript
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,
|
|
})),
|
|
},
|
|
});
|
|
}
|
|
}
|