fix: fix bugs and structure

This commit is contained in:
2026-08-13 09:40:21 +03:30
parent c368948c4b
commit f6a57454f4
18 changed files with 630 additions and 590 deletions
@@ -0,0 +1,235 @@
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.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)' },
);
}
submissionAnswer.choice = choice;
} 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 },
);
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,
})),
},
});
}
}