feat: change answer submission logic

This commit is contained in:
2026-08-09 17:12:55 +03:30
parent b4343bdb53
commit 0c82d3426b
9 changed files with 224 additions and 72 deletions
@@ -1,5 +1,5 @@
import { BadRequestException, HttpStatus, Injectable } from '@nestjs/common';
import { DataSource, In, Repository } from 'typeorm';
import { DataSource, Repository } from 'typeorm';
import { Submission } from '../entities/submission.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { AnswerSubmissionDTO } from '../dtos/answer-submission.dto';
@@ -17,6 +17,10 @@ import { SubmissionAnswer } from '../entities/submission-answer.entity';
import { CalculateResultProvider } from './calculate-result.provider';
import { QuestionsService } from '@/modules/questions/providers/questions.service';
import { SubmissionResult } from '../entities/submission-result.entity';
import { QuestionTransitionType } from '@/modules/questions/enums/question-transition-type.enum';
import { SingleChoiceCondition } from '@/modules/questions/interfaces/single-choice-condition.interface';
import { NumericCondition } from '@/modules/questions/interfaces/numeric-condition.interface';
import { SubmissionStatus } from '../enums/submission-status.enum';
@Injectable()
export class AnswerSubmissionProvider {
@@ -94,6 +98,10 @@ export class AnswerSubmissionProvider {
where: { id: submissionId, user: { id: user.id } },
relations: {
test: true,
currentQuestion: {
choices: true,
outgoingTransitions: true,
},
},
});
} catch (err) {
@@ -108,23 +116,7 @@ export class AnswerSubmissionProvider {
);
}
const [currentQuestion, nextQuestion]: Array<Question | null> =
await this.questionsService.findByOptionsForAdmin({
where: {
test: { id: submission.test.id },
order: In([
submission.currentQuestionOrder,
submission.currentQuestionOrder + 1,
]),
},
relations: {
choices: true,
rangeRules: true,
},
order: {
order: 'ASC',
},
});
const currentQuestion = submission.currentQuestion;
if (!currentQuestion) {
let result: SubmissionResult | null;
@@ -163,7 +155,7 @@ export class AnswerSubmissionProvider {
if (
currentQuestion.type === QuestionType.MULTI_CHOICE &&
answerSubmissionDto.choiceId
answerSubmissionDto.choiceId !== undefined
) {
const choice = currentQuestion.choices.find(
(el) => el.id === answerSubmissionDto.choiceId,
@@ -185,13 +177,29 @@ export class AnswerSubmissionProvider {
submissionAnswer.numericValue = answerSubmissionDto.numericValue;
}
const nextTransition = this.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 (!nextQuestion) {
if (
nextTransition.type === QuestionTransitionType.END ||
!nextQuestion
) {
await manager.update(
Submission,
{ id: submission.id },
{ currentQuestion: null, status: SubmissionStatus.COMPLETED },
);
const result = await this.calculateResultProvider.calculate(
submission,
manager,
@@ -204,11 +212,10 @@ export class AnswerSubmissionProvider {
};
}
await manager.increment(
await manager.update(
Submission,
{ id: submission.id },
'currentQuestionOrder',
1,
{ currentQuestion: nextQuestion },
);
return null;
@@ -225,11 +232,11 @@ export class AnswerSubmissionProvider {
isCompleted: false,
// isFinal: ,
currentQuestion: {
type: nextQuestion.type,
content: nextQuestion.content,
required: nextQuestion.required,
metadata: nextQuestion.metadata,
choices: nextQuestion.choices.map((el) => ({
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,
@@ -296,4 +303,59 @@ export class AnswerSubmissionProvider {
}
}
}
private getNextTransition(
question: Question,
answerSubmissionDto: AnswerSubmissionDTO,
) {
const transitions = question.outgoingTransitions ?? [];
const endTransition = transitions.find(
(t) => t.type === QuestionTransitionType.END,
);
if (
question.type === QuestionType.MULTI_CHOICE &&
answerSubmissionDto.choiceId !== undefined
) {
const transition = transitions.find(
(t) =>
t.type === QuestionTransitionType.CHOICE &&
(t.condition as SingleChoiceCondition).choiceId ===
answerSubmissionDto.choiceId,
);
if (transition) return transition;
}
if (
question.type === QuestionType.NUMERIC &&
answerSubmissionDto.numericValue !== undefined
) {
const value = answerSubmissionDto.numericValue;
const transition = transitions.find((t) => {
if (t.type !== QuestionTransitionType.NUMBER_RANGE) return false;
const condition = t.condition as NumericCondition;
return value >= condition.min && value <= condition.max;
});
if (transition) return transition;
}
const defaultTransition = transitions.find(
(t) => t.type === QuestionTransitionType.DEFAULT,
);
if (defaultTransition) return defaultTransition;
if (endTransition) return endTransition;
throw new AppException(
'submissions.errors.nextQuestionNotFound',
HttpStatus.BAD_REQUEST,
);
}
}
@@ -5,6 +5,7 @@ import {
BadRequestException,
ForbiddenException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { Repository } from 'typeorm';
import { Submission } from '../entities/submission.entity';
@@ -17,7 +18,8 @@ import { randomUUID } from 'crypto';
import { RedisService } from '@/common/modules/redis/providers/redis.service';
import { GuestSubmissionSession } from '../interfaces/guest-submission-session.interfaces';
import { TestStatus } from '@/modules/tests/enums/test-status.enum';
import { QuestionTransitionService } from '@/modules/questions/providers/transition/question-transition.service';
import { QuestionTransition } from '@/modules/questions/entities/question-transition.entity';
import { QuestionTransitionType } from '@/modules/questions/enums/question-transition-type.enum';
@Injectable()
export class StartSubmissionProvider {
@@ -39,42 +41,14 @@ export class StartSubmissionProvider {
private readonly redisService: RedisService,
/**
* Inject Transitions Service
* Inject QuestionTransition Repository
*/
private readonly transitionsService: QuestionTransitionService,
@InjectRepository(QuestionTransition)
private readonly transitionsRepository: Repository<QuestionTransition>,
) {}
public async start(testId: number, user?: User) {
const test = await this.testsService.adminGetOne(testId, {
relations: {
questions: {
choices: true,
},
},
order: {
questions: {
order: 'ASC',
choices: {
order: 'ASC',
},
},
},
select: {
questions: {
id: true,
order: true,
type: true,
content: true,
required: true,
metadata: true,
choices: {
id: true,
order: true,
text: true,
},
},
},
});
const test = await this.testsService.adminGetOne(testId);
if (!test.isActive || test.status !== TestStatus.PUBLISHED) {
throw new ForbiddenException('tests.errors.notActiveTest');
@@ -91,6 +65,54 @@ export class StartSubmissionProvider {
return await this.startGuest(test);
}
private async getFirstQuestion(testId: number) {
let transition: QuestionTransition | null = null;
try {
transition = await this.transitionsRepository.findOne({
where: {
test: { id: testId },
type: QuestionTransitionType.START,
},
relations: {
toQuestion: {
choices: true,
rangeRules: true,
},
},
order: {
toQuestion: {
choices: {
order: 'ASC',
},
},
},
select: {
toQuestion: {
id: true,
type: true,
content: true,
required: true,
metadata: true,
choices: {
id: true,
order: true,
text: true,
},
},
},
});
} catch (err) {
throw new AppTimeoutException(err);
}
if (!transition || !transition.toQuestion) {
throw new NotFoundException('submissions.errors.transitionNotFound');
}
return transition.toQuestion;
}
private async startAuthenticated(test: Test, user: User) {
let submission: Submission | null = null;
@@ -113,10 +135,13 @@ export class StartSubmissionProvider {
throw new BadRequestException('submissions.errors.alreadyTookTest');
}
const firstQuestion = await this.getFirstQuestion(test.id);
if (!submission) {
submission = this.submissionsRepository.create({
test,
user: user || undefined,
currentQuestion: firstQuestion,
});
try {
@@ -126,22 +151,22 @@ export class StartSubmissionProvider {
}
}
// const startTransition = await this.transitionsService.
return new AppResponse({
submissionId: submission.id,
question: test.questions.length > 0 ? test.questions[0] : null,
question: firstQuestion,
});
}
private async startGuest(test: Test) {
const firstQuestion = await this.getFirstQuestion(test.id);
const guestSubmissionId = randomUUID();
await this.redisService.set<GuestSubmissionSession>(
`guest_submission:${guestSubmissionId}`,
{
testId: test.id,
currentQuestion: 0,
currentQuestionId: firstQuestion.id,
answers: [],
},
3600,
@@ -149,7 +174,7 @@ export class StartSubmissionProvider {
return new AppResponse({
guestSubmissionId,
question: test.questions[0],
question: firstQuestion,
});
}
}