336 lines
9.3 KiB
TypeScript
336 lines
9.3 KiB
TypeScript
import { BadRequestException, HttpStatus, Injectable } from '@nestjs/common';
|
|
import { Repository } from 'typeorm';
|
|
import { Submission } from '../entities/submission.entity';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
import { AnswerSubmissionDTO } from '../dtos/answer-submission.dto';
|
|
import { AppTimeoutException } from '@/common/exceptions/app-timeout.exception';
|
|
import { User } from '@/modules/users/entities/user.entity';
|
|
import { AppException } from '@/common/exceptions/app.exception';
|
|
import { RedisService } from '@/common/modules/redis/providers/redis.service';
|
|
import { isUUID } from 'class-validator';
|
|
import { GuestSubmissionSession } from '../interfaces/guest-submission-session.interfaces';
|
|
import { Question } from '@/modules/questions/entities/question.entity';
|
|
import { AppResponse } from '@/common/responses';
|
|
import { QuestionType } from '@/modules/questions/enums/question-type.enum';
|
|
import { NumericMetadata } from '@/modules/questions/interfaces/numeric-metadata.interface';
|
|
import { SubmissionAnswer } from '../entities/submission-answer.entity';
|
|
import { ChoicesService } from '@/modules/questions/providers/choices.service';
|
|
import { CalculateResultProvider } from './calculate-result.provider';
|
|
import { QuestionsService } from '@/modules/questions/providers/questions.service';
|
|
import { SubmissionResult } from '../entities/submission-result.entity';
|
|
|
|
@Injectable()
|
|
export class AnswerSubmissionProvider {
|
|
constructor(
|
|
/**
|
|
* Inject Submission Repository
|
|
*/
|
|
@InjectRepository(Submission)
|
|
private readonly submissionRepository: Repository<Submission>,
|
|
|
|
/**
|
|
* Inject Redis Service
|
|
*/
|
|
private readonly redisService: RedisService,
|
|
|
|
/**
|
|
* Inject SubmisionAnswer Repository
|
|
*/
|
|
@InjectRepository(SubmissionAnswer)
|
|
private readonly submissionAnswerRepository: Repository<SubmissionAnswer>,
|
|
|
|
/**
|
|
* Inject SubmissionResult Repository
|
|
*/
|
|
@InjectRepository(SubmissionResult)
|
|
private readonly submissionResultRepository: Repository<SubmissionResult>,
|
|
|
|
/**
|
|
* Inject Questions Service
|
|
*/
|
|
private readonly questionsService: QuestionsService,
|
|
|
|
/**
|
|
* Inject Choices Service
|
|
*/
|
|
private readonly choicesService: ChoicesService,
|
|
|
|
/**
|
|
* Inject CalculateResult Provider
|
|
*/
|
|
private readonly calculateResultProvider: CalculateResultProvider,
|
|
) {}
|
|
|
|
public async answer(
|
|
id: string,
|
|
answerSubmissionDto: AnswerSubmissionDTO,
|
|
user?: User,
|
|
) {
|
|
if (user) {
|
|
return await this.answerAuthenticated(id, answerSubmissionDto, user);
|
|
} else {
|
|
return await this.answerGuest(id, answerSubmissionDto);
|
|
}
|
|
}
|
|
|
|
private async answerAuthenticated(
|
|
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,
|
|
},
|
|
});
|
|
} catch (err) {
|
|
throw new AppTimeoutException(err);
|
|
}
|
|
|
|
if (!submission) {
|
|
throw new AppException(
|
|
'common.errors.entityNotFound',
|
|
HttpStatus.NOT_FOUND,
|
|
{ entity: '$t(submissions.fields.submission)' },
|
|
);
|
|
}
|
|
|
|
// if (submission.user.id !== user.id) {
|
|
// throw new ForbiddenException('auth.errors.accessDenied');
|
|
// }
|
|
|
|
// const allQuestions = [...submission.test.questions].sort(
|
|
// (a, b) => a.order - b.order,
|
|
// );
|
|
// const answeredQuestions = submission.answers.map((el) => el.question);
|
|
|
|
// const questionStats = this.getQuestionStats(
|
|
// allQuestions,
|
|
// answeredQuestions,
|
|
// );
|
|
|
|
const currentQuestion: Question | null =
|
|
await this.questionsService.findOneForAdmin({
|
|
where: {
|
|
test: { id: submission.test.id },
|
|
order: submission.currentQuestionOrder,
|
|
},
|
|
relations: {
|
|
choices: true,
|
|
rangeRules: true,
|
|
},
|
|
});
|
|
|
|
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,
|
|
});
|
|
}
|
|
|
|
this.checkAnswerCorrectness(currentQuestion, answerSubmissionDto);
|
|
|
|
const submissionAnswer = this.submissionAnswerRepository.create({
|
|
submission,
|
|
question: currentQuestion,
|
|
});
|
|
|
|
if (
|
|
currentQuestion.type === QuestionType.MULTI_CHOICE &&
|
|
answerSubmissionDto.choiceId
|
|
) {
|
|
const choice = await this.choicesService.findOneForAdmin(
|
|
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
|
|
) {
|
|
submissionAnswer.numericValue = answerSubmissionDto.numericValue;
|
|
}
|
|
|
|
try {
|
|
await this.submissionAnswerRepository.save(submissionAnswer);
|
|
} catch (err) {
|
|
throw new AppTimeoutException(err);
|
|
}
|
|
|
|
const nextQuestion: Question | null =
|
|
await this.questionsService.findOneForAdmin({
|
|
where: {
|
|
test: { id: submission.test.id },
|
|
order: submission.currentQuestionOrder + 1,
|
|
},
|
|
relations: {
|
|
choices: true,
|
|
rangeRules: true,
|
|
},
|
|
});
|
|
|
|
if (!nextQuestion) {
|
|
const result = await this.calculateResultProvider.calculate(submission);
|
|
return new AppResponse({
|
|
isCompleted: true,
|
|
currentQuestion: null,
|
|
result,
|
|
});
|
|
}
|
|
|
|
return new AppResponse({
|
|
isCompleted: false,
|
|
// isFinal: ,
|
|
currentQuestion: {
|
|
type: nextQuestion.type,
|
|
content: nextQuestion.content,
|
|
required: nextQuestion.required,
|
|
metadata: nextQuestion.metadata,
|
|
choices: nextQuestion.choices,
|
|
},
|
|
});
|
|
}
|
|
|
|
private async answerGuest(
|
|
id: string,
|
|
answerSubmissionDto: AnswerSubmissionDTO,
|
|
) {
|
|
console.log(id, answerSubmissionDto);
|
|
|
|
if (!isUUID(id)) {
|
|
throw new AppException(
|
|
'validation.wrongFieldFormat',
|
|
HttpStatus.BAD_REQUEST,
|
|
{ field: '$t(submissions.fields.submissionId)' },
|
|
);
|
|
}
|
|
|
|
const submission = await this.redisService.get<GuestSubmissionSession>(
|
|
`guest_submission:${id}`,
|
|
);
|
|
|
|
return submission;
|
|
}
|
|
|
|
private getQuestionStats(
|
|
allQuestions: Array<Question>,
|
|
answeredQuestions: Array<Question>,
|
|
) {
|
|
const questionStats: Record<'prev' | 'current' | 'next', Question | null> =
|
|
{
|
|
prev: null,
|
|
current: null,
|
|
next: null,
|
|
};
|
|
|
|
if (allQuestions.length === 0) {
|
|
return questionStats;
|
|
}
|
|
|
|
if (answeredQuestions.length > 0) {
|
|
questionStats.prev = answeredQuestions[answeredQuestions.length - 1];
|
|
}
|
|
|
|
if (!questionStats.prev) {
|
|
questionStats.current = allQuestions[0];
|
|
} else {
|
|
const prevQuestionIdx = allQuestions.findIndex(
|
|
(val) => val.id === questionStats.prev?.id,
|
|
);
|
|
|
|
questionStats.current = allQuestions[prevQuestionIdx + 1];
|
|
}
|
|
|
|
if (questionStats.current) {
|
|
const currentQuestionIdx = allQuestions.findIndex(
|
|
(val) => val.id === questionStats.current?.id,
|
|
);
|
|
|
|
questionStats.next = allQuestions[currentQuestionIdx + 1];
|
|
}
|
|
|
|
return questionStats;
|
|
}
|
|
|
|
private checkAnswerCorrectness(
|
|
question: Question,
|
|
answerSubmissionDto: AnswerSubmissionDTO,
|
|
) {
|
|
if (!answerSubmissionDto) throw new BadRequestException();
|
|
|
|
const { choiceId, numericValue } = answerSubmissionDto;
|
|
|
|
if (question.type === QuestionType.NUMERIC) {
|
|
if (numericValue === undefined) {
|
|
throw new BadRequestException('submissions.errors.emptyNumericValue');
|
|
}
|
|
|
|
const metadata = question.metadata as NumericMetadata;
|
|
|
|
if (
|
|
metadata &&
|
|
(numericValue < metadata.min || numericValue > metadata.max)
|
|
) {
|
|
throw new BadRequestException('submissions.errors.numericValueRange');
|
|
}
|
|
}
|
|
|
|
if (question.type === QuestionType.MULTI_CHOICE) {
|
|
if (!choiceId) {
|
|
throw new BadRequestException('submissions.errors.emptyChoiceId');
|
|
}
|
|
|
|
if (!question.choices.some((c) => c.id === choiceId)) {
|
|
throw new AppException(
|
|
'validation.wrongField',
|
|
HttpStatus.BAD_REQUEST,
|
|
{ field: '$t(submissions.fields.choiceId)' },
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|