254 lines
7.2 KiB
TypeScript
254 lines
7.2 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import {
|
|
EntityManager,
|
|
In,
|
|
LessThanOrEqual,
|
|
MoreThanOrEqual,
|
|
Repository,
|
|
} from 'typeorm';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
|
|
import { SubmissionResult } from '../../entities/submission-result.entity';
|
|
import { Submission } from '../../entities/submission.entity';
|
|
import { QuestionType } from '@/modules/questions/enums/question-type.enum';
|
|
import { AppTimeoutException } from '@/common/exceptions/app-timeout.exception';
|
|
import { SubmissionAnswer } from '../../entities/submission-answer.entity';
|
|
import { Question } from '@/modules/questions/entities/question.entity';
|
|
import { ResultRange } from '@/modules/scoring/entities/result-range.entity';
|
|
import { GuestSubmissionSession } from '../../interfaces/guest-submission-session.interface';
|
|
|
|
@Injectable()
|
|
export class CalculateResultProvider {
|
|
constructor(
|
|
/**
|
|
* Inject Question Repository
|
|
*/
|
|
@InjectRepository(Question)
|
|
private readonly questionRepository: Repository<Question>,
|
|
|
|
/**
|
|
* Inject Result Range Repository
|
|
*/
|
|
@InjectRepository(ResultRange)
|
|
private readonly resultRangeRepository: Repository<ResultRange>,
|
|
) {}
|
|
|
|
public async calculateAuthenticated(
|
|
submission: Submission,
|
|
manager: EntityManager,
|
|
) {
|
|
let submissionAnswers: SubmissionAnswer[] = [];
|
|
|
|
try {
|
|
submissionAnswers = await manager.find(SubmissionAnswer, {
|
|
where: { submission: { id: submission.id } },
|
|
relations: {
|
|
question: {
|
|
rangeRules: true,
|
|
},
|
|
choice: true,
|
|
choices: true,
|
|
},
|
|
});
|
|
} catch (err) {
|
|
throw new AppTimeoutException(err);
|
|
}
|
|
|
|
const result = await this.calculateFromAnswers(
|
|
submissionAnswers.map((answer) => ({
|
|
questionId: answer.question.id,
|
|
question: answer.question,
|
|
choiceId: answer.choice?.id,
|
|
choice: answer.choice,
|
|
choiceIds: answer.choices?.map((choice) => choice.id),
|
|
choices: answer.choices,
|
|
numericValue: answer.numericValue,
|
|
})),
|
|
async (questionIds) =>
|
|
manager.find(Question, {
|
|
where: { id: In(questionIds) },
|
|
relations: {
|
|
choices: true,
|
|
rangeRules: true,
|
|
},
|
|
}),
|
|
async (probability) =>
|
|
manager.find(ResultRange, {
|
|
where: {
|
|
test: { id: submission.test.id },
|
|
minProbability: LessThanOrEqual(probability),
|
|
maxProbability: MoreThanOrEqual(probability),
|
|
},
|
|
}),
|
|
);
|
|
|
|
const submissionResult = manager.create(SubmissionResult, {
|
|
submission,
|
|
score: result.score,
|
|
probability: result.probability,
|
|
resultRange: result.resultRange,
|
|
});
|
|
|
|
try {
|
|
await manager.save(submissionResult);
|
|
} catch (err) {
|
|
throw new AppTimeoutException(err);
|
|
}
|
|
|
|
return {
|
|
score: result.score,
|
|
probability: result.probability,
|
|
title: result.resultRange?.title,
|
|
description: result.resultRange?.description,
|
|
};
|
|
}
|
|
|
|
public async calculateGuest(submission: GuestSubmissionSession) {
|
|
const result = await this.calculateFromAnswers(
|
|
submission.answers.map((answer) => ({
|
|
questionId: answer.questionId,
|
|
choiceId: answer.choiceId,
|
|
choiceIds: answer.choiceIds,
|
|
numericValue: answer.numericValue,
|
|
})),
|
|
async (questionIds) =>
|
|
this.questionRepository.find({
|
|
where: { id: In(questionIds) },
|
|
relations: {
|
|
choices: true,
|
|
rangeRules: true,
|
|
},
|
|
}),
|
|
async (probability) =>
|
|
this.resultRangeRepository.find({
|
|
where: {
|
|
test: { id: submission.testId },
|
|
minProbability: LessThanOrEqual(probability),
|
|
maxProbability: MoreThanOrEqual(probability),
|
|
},
|
|
}),
|
|
);
|
|
|
|
submission.result = {
|
|
score: result.score,
|
|
probability: result.probability,
|
|
title: result.resultRange?.title || '',
|
|
description: result.resultRange?.description || '',
|
|
};
|
|
|
|
return submission.result;
|
|
}
|
|
|
|
private async calculateFromAnswers(
|
|
answers: Array<{
|
|
questionId: number;
|
|
question?: Question;
|
|
choiceId?: number;
|
|
choice?: { id: number; score: number };
|
|
choiceIds?: Array<number>;
|
|
choices?: Array<{ id: number; score: number }>;
|
|
numericValue?: number;
|
|
}>,
|
|
loadQuestions: (questionIds: number[]) => Promise<Question[]>,
|
|
loadResultRanges: (probability: number) => Promise<ResultRange[]>,
|
|
) {
|
|
let score = 0;
|
|
|
|
const questionIds = answers.map((a) => a.questionId);
|
|
|
|
const questions = await loadQuestions(questionIds);
|
|
|
|
const questionsMap = new Map<number, Question>();
|
|
|
|
questions.forEach((q) => questionsMap.set(q.id, q));
|
|
|
|
for (const answer of answers) {
|
|
const question = answer.question ?? questionsMap.get(answer.questionId);
|
|
|
|
if (!question) continue;
|
|
|
|
if (
|
|
question.type === QuestionType.SINGLE_CHOICE &&
|
|
answer.choiceId !== undefined
|
|
) {
|
|
const choice =
|
|
answer.choice ??
|
|
question.choices.find((c) => c.id === answer.choiceId);
|
|
|
|
if (choice) {
|
|
score += question.weight * choice.score;
|
|
}
|
|
} else if (
|
|
question.type === QuestionType.MULTI_CHOICE &&
|
|
answer.choiceIds !== undefined
|
|
) {
|
|
const selectedChoiceIds = new Set(answer.choiceIds);
|
|
|
|
const selectedChoices =
|
|
answer.choices ??
|
|
question.choices.filter((choice) => selectedChoiceIds.has(choice.id));
|
|
|
|
const multiChoiceScore = selectedChoices.reduce(
|
|
(sum, choice) => sum + choice.score,
|
|
0,
|
|
);
|
|
|
|
score += question.weight * multiChoiceScore;
|
|
} else if (
|
|
question.type === QuestionType.NUMERIC &&
|
|
answer.numericValue !== undefined
|
|
) {
|
|
question.rangeRules?.forEach((rule) => {
|
|
if (
|
|
answer.numericValue! >= rule.min &&
|
|
answer.numericValue! <= rule.max
|
|
) {
|
|
score += question.weight * rule.score;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
let totalScores = 0;
|
|
|
|
questions.forEach((question) => {
|
|
if (question.type === QuestionType.SINGLE_CHOICE) {
|
|
const maxScore =
|
|
question.choices.length > 0
|
|
? Math.max(...question.choices.map((choice) => choice.score))
|
|
: 0;
|
|
|
|
totalScores += question.weight * maxScore;
|
|
} else if (question.type === QuestionType.MULTI_CHOICE) {
|
|
const maxScore = question.choices.reduce(
|
|
(sum, choice) => sum + choice.score,
|
|
0,
|
|
);
|
|
|
|
totalScores += question.weight * maxScore;
|
|
} else if (
|
|
question.type === QuestionType.NUMERIC &&
|
|
question.rangeRules &&
|
|
question.rangeRules.length > 0
|
|
) {
|
|
const maxScore = Math.max(
|
|
...question.rangeRules.map((rule) => rule.score),
|
|
);
|
|
|
|
totalScores += question.weight * maxScore;
|
|
}
|
|
});
|
|
|
|
const probability =
|
|
totalScores > 0 ? Math.ceil((score / totalScores) * 100) : 0;
|
|
|
|
const resultRanges = await loadResultRanges(probability);
|
|
|
|
return {
|
|
score,
|
|
probability,
|
|
resultRange: resultRanges.length > 0 ? resultRanges[0] : undefined,
|
|
};
|
|
}
|
|
}
|