249 lines
7.0 KiB
TypeScript
249 lines
7.0 KiB
TypeScript
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<Submission>,
|
|
|
|
/**
|
|
* Inject SubmisionAnswer Repository
|
|
*/
|
|
@InjectRepository(SubmissionAnswer)
|
|
private readonly submissionAnswerRepository: Repository<SubmissionAnswer>,
|
|
|
|
/**
|
|
* Inject SubmissionResult Repository
|
|
*/
|
|
@InjectRepository(SubmissionResult)
|
|
private readonly submissionResultRepository: Repository<SubmissionResult>,
|
|
|
|
/**
|
|
* 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.MULTI_CHOICE &&
|
|
answerSubmissionDto.choiceIds !== undefined
|
|
) {
|
|
const choices = currentQuestion.choices.filter((el) =>
|
|
answerSubmissionDto.choiceIds?.includes(el.id),
|
|
);
|
|
|
|
submissionAnswer.choices = choices;
|
|
} 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,
|
|
completedAt: new Date(),
|
|
},
|
|
);
|
|
|
|
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,
|
|
})),
|
|
},
|
|
});
|
|
}
|
|
}
|