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, /** * Inject Result Range Repository */ @InjectRepository(ResultRange) private readonly resultRangeRepository: Repository, ) {} 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; choices?: Array<{ id: number; score: number }>; numericValue?: number; }>, loadQuestions: (questionIds: number[]) => Promise, loadResultRanges: (probability: number) => Promise, ) { let score = 0; const questionIds = answers.map((a) => a.questionId); const questions = await loadQuestions(questionIds); const questionsMap = new Map(); 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, }; } }