File

apps/recallassess/recallassess-api/src/api/client/knowledge-review/knowledge-review.service.ts

Index

Methods

Constructor

constructor(prisma: BNestPrismaService, eventEmitter: EventEmitter2, subscriptionCourseAccess: ParticipantSubscriptionCourseAccessService)
Parameters :
Name Type Optional
prisma BNestPrismaService No
eventEmitter EventEmitter2 No
subscriptionCourseAccess ParticipantSubscriptionCourseAccessService No

Methods

Async getKnowledgeReviewByLearningGroupParticipantId
getKnowledgeReviewByLearningGroupParticipantId(learningGroupParticipantId: number, participantId: number)

Get knowledge review quiz for a course

Parameters :
Name Type Optional Description
learningGroupParticipantId number No

LearningGroupParticipant ID

participantId number No

Participant ID (for validation)

Returns : Promise<literal type>

Knowledge review with questions and answers

Async getKnowledgeReviewSubmissionStatus
getKnowledgeReviewSubmissionStatus(knowledgeReviewId: number, participantId: number, courseModulePageId?: number | null)

Get knowledge review submission status and submitted answers

Parameters :
Name Type Optional Description
knowledgeReviewId number No

Knowledge Review ID

participantId number No

Participant ID

courseModulePageId number | null Yes

Optional course module page ID (for embedded quizzes). If null, checks standalone KR.

Returns : Promise<literal type>

Submission status and submitted answers if completed

Private shuffleArray
shuffleArray(array: T[])
Type parameters :
  • T

Shuffle array using Fisher-Yates algorithm

Parameters :
Name Type Optional Description
array T[] No

Array to shuffle

Returns : T[]

New shuffled array

Async submitKnowledgeReview
submitKnowledgeReview(submitDto: SubmitKnowledgeReviewDto, participantId: number)

Submit knowledge review answers

Parameters :
Name Type Optional Description
submitDto SubmitKnowledgeReviewDto No

Submission data

participantId number No

Participant ID

Returns : Promise<literal type>

Created knowledge review participant record

import { ParticipantSubscriptionCourseAccessService } from "@api/client/shared/participant-subscription-course-access.service";
import { bnestPlainToDto, bnestPlainToDtoArray } from "@bish-nest/core";
import { BNestPrismaService } from "@bish-nest/core/services";
import { ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
import { EventEmitter2 } from "@nestjs/event-emitter";
import { ContentType, KnowledgeReviewQuestionType } from "@prisma/client";

// Temporary: Use string literals until TypeScript picks up the regenerated Prisma client
// After restarting TypeScript server, this should work: import { KnowledgeReviewSubmissionType } from "@prisma/client";
type KnowledgeReviewSubmissionType = "COURSE_LEVEL" | "EMBEDDED_QUIZ" | "EMBEDDED_ASSIGNMENT";
const KnowledgeReviewSubmissionType = {
  COURSE_LEVEL: "COURSE_LEVEL" as const,
  EMBEDDED_QUIZ: "EMBEDDED_QUIZ" as const,
  EMBEDDED_ASSIGNMENT: "EMBEDDED_ASSIGNMENT" as const,
};

import {
  COURSE_PROGRESS_EVENTS,
  KnowledgeReviewCompletedEvent,
} from "../learning-group/events/course-progress.events";
import {
  CLKnowledgeReviewAnswerDto,
  CLKnowledgeReviewDto,
  CLKnowledgeReviewQuestionDto,
  SubmitKnowledgeReviewDto,
} from "./dto";

@Injectable()
export class CLKnowledgeReviewService {
  constructor(
    private readonly prisma: BNestPrismaService,
    private readonly eventEmitter: EventEmitter2,
    private readonly subscriptionCourseAccess: ParticipantSubscriptionCourseAccessService,
  ) {}

  /**
   * Shuffle array using Fisher-Yates algorithm
   * @param array Array to shuffle
   * @returns New shuffled array
   */
  private shuffleArray<T>(array: T[]): T[] {
    const shuffled = [...array];
    for (let i = shuffled.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
    }
    return shuffled;
  }

  /**
   * Get knowledge review quiz for a course
   * @param learningGroupParticipantId LearningGroupParticipant ID
   * @param participantId Participant ID (for validation)
   * @returns Knowledge review with questions and answers
   */
  async getKnowledgeReviewByLearningGroupParticipantId(
    learningGroupParticipantId: number,
    participantId: number,
  ): Promise<{
    knowledgeReview: CLKnowledgeReviewDto;
    questions: CLKnowledgeReviewQuestionDto[];
    isCompleted: boolean;
  }> {
    // Get enrollment using LearningGroupParticipant ID
    const enrollment = await this.prisma.client.learningGroupParticipant.findFirst({
      where: {
        id: learningGroupParticipantId,
        participant_id: participantId,
      },
    });

    if (!enrollment) {
      throw new NotFoundException("Course enrollment not found or access denied");
    }

    if (enrollment.cancelled) {
      throw new ForbiddenException("This course license has been cancelled. You no longer have access to this knowledge review.");
    }

    // Type assertion to access course_id
    const enrollmentWithCourseId = enrollment as typeof enrollment & { course_id: number };
    const courseId = enrollmentWithCourseId.course_id;

    // Get course to access knowledge_review_id
    const course = await this.prisma.client.course.findUnique({
      where: {
        id: courseId,
      },
    });

    if (!course) {
      throw new NotFoundException("Course not found");
    }

    // Type assertion to access knowledge_review_id
    const courseWithKnowledgeReviewId = course as typeof course & { knowledge_review_id: number | null };

    if (!courseWithKnowledgeReviewId.knowledge_review_id) {
      throw new NotFoundException(
        `Knowledge review quiz not found for course ID ${courseId}. Please ensure a knowledge review is assigned to this course.`,
      );
    }

    // Get knowledge review
    const knowledgeReview = await this.prisma.client.knowledgeReview.findUnique({
      where: {
        id: courseWithKnowledgeReviewId.knowledge_review_id,
      },
    });

    if (!knowledgeReview) {
      throw new NotFoundException("Knowledge review not found");
    }

    await this.subscriptionCourseAccess.assertAllowsKnowledgeReviewRead(
      participantId,
      learningGroupParticipantId,
      enrollment.status,
      courseId,
      knowledgeReview.id,
    );

    // Get questions with answers
    const questions = await this.prisma.client.knowledgeReviewQuestion.findMany({
      where: {
        knowledge_review_id: knowledgeReview.id,
      },
      include: {
        knowledgeReviewAnswers: {
          orderBy: {
            sort_order: "asc",
          },
        },
      },
      orderBy: {
        sort_order: "asc",
      },
    });

    // Check if already completed and get submitted answers
    // For standalone KR, course_module_page_id must be NULL
    // This distinguishes it from embedded quizzes which have course_module_page_id set
    const existingParticipant = await this.prisma.client.knowledgeReviewParticipant.findFirst({
      where: {
        knowledge_review_id: knowledgeReview.id,
        participant_id: participantId,
        course_module_page_id: null, // Only check standalone KR, not embedded quizzes
      } as any, // Type assertion needed until TypeScript server picks up regenerated Prisma types
    });

    const isCompleted = !!existingParticipant;

    // Get submitted answers if completed
    const submittedAnswers: Map<number, { answerId?: number; answerText?: string }> = new Map();
    if (isCompleted) {
      const results = await this.prisma.client.knowledgeReviewResult.findMany({
        where: {
          knowledge_review_id: knowledgeReview.id,
          participant_id: participantId,
          course_module_page_id: null, // Only get results for standalone KR, not embedded quizzes
        } as any, // Type assertion needed until TypeScript server picks up regenerated Prisma types
        include: {
          knowledgeReviewQuestion: true,
        },
      });

      results.forEach((result) => {
        const questionId = result.knowledge_review_question_id;
        const selectedAnswers = result.selected_answers as any;

        if (selectedAnswers) {
          // Handle text input answers from JSON (check first, as it's mutually exclusive with multiple choice)
          if (selectedAnswers.answer_text !== undefined && selectedAnswers.answer_text !== null) {
            submittedAnswers.set(questionId, { answerText: selectedAnswers.answer_text });
          }
          // Handle multiple choice answers from JSON
          else if (selectedAnswers.knowledge_review_answer_id) {
            submittedAnswers.set(questionId, { answerId: selectedAnswers.knowledge_review_answer_id });
          }
        } else if (result.knowledge_review_answer_id) {
          // Handle multiple choice answers from direct field (fallback)
          submittedAnswers.set(questionId, { answerId: result.knowledge_review_answer_id });
        }
      });
    }

    // Map questions to include answers in the correct format
    // Prisma returns knowledgeReviewAnswers, but DTO expects answers
    // Preserve the order from the document (answers are already ordered by sort_order from DB query)
    const questionsWithAnswers = questions.map((question) => {
      const { knowledgeReviewAnswers, ...questionData } = question;
      const submittedAnswer = submittedAnswers.get(question.id);

      // Convert to DTOs - preserve order from database (already sorted by sort_order ASC)
      // This ensures "None of the above" and "All of the above" appear in the 4th position as per document
      const answerDtos = knowledgeReviewAnswers
        ? bnestPlainToDtoArray(knowledgeReviewAnswers, CLKnowledgeReviewAnswerDto)
        : [];

      return {
        ...questionData,
        answers: answerDtos, // Use answers in original order (preserves document order)
        submittedAnswer: submittedAnswer || null,
      };
    });

    return {
      knowledgeReview: bnestPlainToDto(knowledgeReview, CLKnowledgeReviewDto),
      questions: bnestPlainToDtoArray(questionsWithAnswers, CLKnowledgeReviewQuestionDto),
      isCompleted,
    };
  }

  /**
   * Get knowledge review submission status and submitted answers
   * @param knowledgeReviewId Knowledge Review ID
   * @param participantId Participant ID
   * @param courseModulePageId Optional course module page ID (for embedded quizzes). If null, checks standalone KR.
   * @returns Submission status and submitted answers if completed
   */
  async getKnowledgeReviewSubmissionStatus(
    knowledgeReviewId: number,
    participantId: number,
    courseModulePageId?: number | null,
  ): Promise<{
    isCompleted: boolean;
    submittedAnswers?: Record<number, { answerId?: number; answerText?: string }>;
  }> {
    // Check if already completed
    // For embedded quizzes: filter by course_module_page_id
    // For standalone KR: course_module_page_id should be NULL
    const whereClause: any = {
      knowledge_review_id: knowledgeReviewId,
      participant_id: participantId,
    };
    if (courseModulePageId !== undefined) {
      whereClause.course_module_page_id = courseModulePageId;
    } else {
      whereClause.course_module_page_id = null;
    }
    const existingParticipant = await this.prisma.client.knowledgeReviewParticipant.findFirst({
      where: whereClause,
    });

    const isCompleted = !!existingParticipant;

    if (!isCompleted) {
      return { isCompleted: false };
    }

    // Get submitted answers
    // Filter by course_module_page_id to distinguish between embedded quizzes and standalone KR
    const resultsWhereClause: any = {
      knowledge_review_id: knowledgeReviewId,
      participant_id: participantId,
    };
    if (courseModulePageId !== undefined) {
      resultsWhereClause.course_module_page_id = courseModulePageId;
    } else {
      resultsWhereClause.course_module_page_id = null;
    }
    const results = await this.prisma.client.knowledgeReviewResult.findMany({
      where: resultsWhereClause,
    });

    const submittedAnswers = new Map<number, { answerId?: number; answerText?: string }>();
    results.forEach((result) => {
      const questionId = result.knowledge_review_question_id;
      const selectedAnswers = result.selected_answers as any;

      if (selectedAnswers) {
        // Handle text input answers from JSON
        if (selectedAnswers.answer_text !== undefined && selectedAnswers.answer_text !== null) {
          submittedAnswers.set(questionId, { answerText: selectedAnswers.answer_text });
        }
        // Handle multiple choice answers from JSON
        else if (selectedAnswers.knowledge_review_answer_id) {
          submittedAnswers.set(questionId, { answerId: selectedAnswers.knowledge_review_answer_id });
        }
      } else if (result.knowledge_review_answer_id) {
        // Handle multiple choice answers from direct field (fallback)
        submittedAnswers.set(questionId, { answerId: result.knowledge_review_answer_id });
      }
    });

    // Convert Map to object for JSON serialization
    const submittedAnswersObj: Record<number, { answerId?: number; answerText?: string }> = {};
    submittedAnswers.forEach((value, key) => {
      submittedAnswersObj[key] = value;
    });

    return {
      isCompleted: true,
      submittedAnswers: submittedAnswersObj,
    };
  }

  /**
   * Submit knowledge review answers
   * @param submitDto Submission data
   * @param participantId Participant ID
   * @returns Created knowledge review participant record
   */
  async submitKnowledgeReview(
    submitDto: SubmitKnowledgeReviewDto,
    participantId: number,
  ): Promise<{
    success: boolean;
    knowledgeReviewParticipantId: number | null;
    passed: boolean;
    score: number;
    passPercentage: number | null;
    questionResults: Array<{ questionId: number; isCorrect: boolean }>;
  }> {
    // Verify participant is enrolled in this course
    const enrollment = await this.prisma.client.learningGroupParticipant.findFirst({
      where: {
        participant_id: participantId,
        learningGroup: {
          course_id: submitDto.course_id,
        },
      },
    });

    if (!enrollment) {
      throw new NotFoundException("Course not found or you are not enrolled");
    }

    if (enrollment.cancelled) {
      throw new ForbiddenException("This course license has been cancelled. You can no longer submit this knowledge review.");
    }

    await this.subscriptionCourseAccess.assertAllowsContinuingCourseAccess(
      participantId,
      enrollment.id,
      enrollment.status,
    );

    // Verify knowledge review exists
    const knowledgeReview = await this.prisma.client.knowledgeReview.findUnique({
      where: {
        id: submitDto.knowledge_review_id,
      },
    });

    if (!knowledgeReview) {
      throw new NotFoundException("Knowledge review not found");
    }

    // Verify knowledge review belongs to the course
    // Check if it's a course-level knowledge review OR if it's used in a page within this course
    const course = await this.prisma.client.course.findUnique({
      where: {
        id: submitDto.course_id,
      },
    });

    if (!course) {
      throw new NotFoundException("Course not found");
    }

    const courseWithKnowledgeReviewId = course as typeof course & { knowledge_review_id: number | null };

    // Check if it's a course-level knowledge review
    const isCourseLevelReview = courseWithKnowledgeReviewId.knowledge_review_id === submitDto.knowledge_review_id;

    // Determine course_module_page_id
    // If provided in DTO (for embedded quizzes), use it directly
    // If not provided and it's a course-level review, set to null for standalone KR
    // If not provided and it's NOT a course-level review, it must be an embedded quiz - find the page
    let courseModulePageId: number | null = null;
    let pageWithKnowledgeReview: { id: number; course_module_id: number } | null = null;

    if (submitDto.course_module_page_id !== undefined && submitDto.course_module_page_id !== null) {
      // Page ID explicitly provided (embedded quiz)
      courseModulePageId = submitDto.course_module_page_id;
      // Verify the page exists and belongs to this course and has this knowledge review
      pageWithKnowledgeReview = await this.prisma.client.courseModulePage.findFirst({
        where: {
          id: courseModulePageId,
          course_id: submitDto.course_id,
          knowledge_review_id: submitDto.knowledge_review_id,
        },
      });
      if (!pageWithKnowledgeReview) {
        throw new NotFoundException("Course module page not found or does not match the knowledge review");
      }
    } else {
      // No page ID provided
      if (isCourseLevelReview) {
        // It's a course-level knowledge review - this is a standalone KR
        // courseModulePageId remains null
        pageWithKnowledgeReview = null;
      } else {
        // Not a course-level review, so it must be an embedded quiz
        // Find the page that contains this knowledge review
        pageWithKnowledgeReview = await this.prisma.client.courseModulePage.findFirst({
          where: {
            course_id: submitDto.course_id,
            knowledge_review_id: submitDto.knowledge_review_id,
          },
        });
        if (pageWithKnowledgeReview) {
          courseModulePageId = pageWithKnowledgeReview.id;
        } else {
          throw new NotFoundException(
            "Knowledge review not found as embedded quiz in any page. If this is a standalone knowledge review, ensure it is assigned to the course.",
          );
        }
      }
    }

    if (!isCourseLevelReview && !pageWithKnowledgeReview) {
      throw new NotFoundException("Knowledge review does not belong to this course");
    }

    // Check if already submitted
    // For embedded quizzes: filter by course_module_page_id
    // For standalone KR: course_module_page_id should be NULL
    const submitWhereClause: any = {
      knowledge_review_id: submitDto.knowledge_review_id,
      participant_id: participantId,
      course_module_page_id: courseModulePageId,
    };
    const existingParticipant = await this.prisma.client.knowledgeReviewParticipant.findFirst({
      where: submitWhereClause,
    });

    if (existingParticipant) {
      throw new NotFoundException("Knowledge review quiz has already been submitted");
    }

    // Get questions to validate answers
    const questions = await this.prisma.client.knowledgeReviewQuestion.findMany({
      where: {
        knowledge_review_id: submitDto.knowledge_review_id,
      },
      include: {
        knowledgeReviewAnswers: {
          orderBy: {
            sort_order: "asc",
          },
        },
      },
    });


    // Determine submission_type based on course_module_page_id and page content_type
    let submissionType: KnowledgeReviewSubmissionType;
    if (courseModulePageId === null) {
      // Course-level knowledge review (not embedded)
      submissionType = KnowledgeReviewSubmissionType.COURSE_LEVEL;
    } else {
      // Embedded quiz/assignment - use content_type from the page
      // Get the page to check its content_type
      const page = pageWithKnowledgeReview
        ? await this.prisma.client.courseModulePage.findUnique({
            where: { id: courseModulePageId },
            select: { content_type: true },
          })
        : null;

      // Use content_type from page, or fallback to question types if not set
      // Using string literal for ASSIGNMENT until TypeScript picks up the enum
      if (page?.content_type === ("ASSIGNMENT" as ContentType)) {
        submissionType = KnowledgeReviewSubmissionType.EMBEDDED_ASSIGNMENT;
      } else if (page?.content_type === ContentType.QUIZ) {
        submissionType = KnowledgeReviewSubmissionType.EMBEDDED_QUIZ;
      } else {
        // Fallback: determine type based on question types (for backward compatibility)
        const hasTextInput = questions.some((q) => q.question_type === KnowledgeReviewQuestionType.TEXT_INPUT);
        const hasMultipleChoice = questions.some(
          (q) => q.question_type === KnowledgeReviewQuestionType.MULTIPLE_CHOICE,
        );

        if (hasTextInput && !hasMultipleChoice) {
          submissionType = KnowledgeReviewSubmissionType.EMBEDDED_ASSIGNMENT;
        } else {
          submissionType = KnowledgeReviewSubmissionType.EMBEDDED_QUIZ;
        }
      }
    }

    // Start transaction
    return await this.prisma.client
      .$transaction(async (tx) => {
        // Get course_module_id
        // For page-level quizzes, use the module from the page
        // For course-level quizzes, use the first module
        let courseModuleId: number;

        if (pageWithKnowledgeReview) {
          // Use the module from the page that contains this knowledge review
          courseModuleId = pageWithKnowledgeReview.course_module_id;
        } else {
          // For course-level knowledge reviews, use the first module
          // CourseModule no longer has course_id - find through CourseModulePage
          const firstPage = await tx.courseModulePage.findFirst({
            where: {
              course_id: submitDto.course_id,
              courseModule: {
                is_published: true,
              },
            },
            select: {
              courseModule: {
                select: {
                  id: true,
                  sort_order: true,
                },
              },
            },
            orderBy: {
              courseModule: {
                sort_order: "asc",
              },
            },
          });

          if (!firstPage?.courseModule) {
            throw new NotFoundException("Course must have at least one published module");
          }

          courseModuleId = firstPage.courseModule.id;
        }

        const results = [];
        const questionResults: Array<{ questionId: number; isCorrect: boolean }> = [];
        let correctCount = 0;
        let incorrectCount = 0;

        // Create results for each answer
        for (const answer of submitDto.answers) {
          const question = questions.find((q) => q.id === answer.knowledge_review_question_id);
          if (!question) {
            throw new NotFoundException(`Question ${answer.knowledge_review_question_id} not found`);
          }

          let isCorrect = false;
          let knowledgeReviewAnswerId: number | null = null;

          if (question.question_type === KnowledgeReviewQuestionType.MULTIPLE_CHOICE) {
            // For multiple choice, validate that answer_id is provided
            if (!answer.knowledge_review_answer_id) {
              throw new NotFoundException(
                `Answer ID is required for multiple choice question ${answer.knowledge_review_question_id}`,
              );
            }

            // Check if the selected answer is correct
            // IMPORTANT: Find the answer by ID from the database
            const selectedAnswer = question.knowledgeReviewAnswers.find(
              (a) => a.id === answer.knowledge_review_answer_id,
            );
            if (!selectedAnswer) {
              console.error(`[Quiz Debug ERROR] Answer ${answer.knowledge_review_answer_id} not found in database for question ${answer.knowledge_review_question_id}`);
              console.error(`[Quiz Debug ERROR] Available answer IDs for this question:`, question.knowledgeReviewAnswers.map(a => a.id));
              throw new NotFoundException(
                `Answer ${answer.knowledge_review_answer_id} not found for question ${answer.knowledge_review_question_id}`,
              );
            }

            isCorrect = selectedAnswer.is_correct;
            knowledgeReviewAnswerId = selectedAnswer.id;
          } else if (question.question_type === KnowledgeReviewQuestionType.TEXT_INPUT) {
            // For text input, check if answer_text is provided
            if (!answer.answer_text || answer.answer_text.trim() === "") {
              throw new NotFoundException(
                `Answer text is required for text input question ${answer.knowledge_review_question_id}`,
              );
            }

            // For text input, we can't automatically determine correctness
            // You might want to implement manual review or keyword matching here
            // For now, we'll store the text answer but mark as not correct by default
            isCorrect = false;
          }

          // Store question result for response (before deletion if failed)
          questionResults.push({
            questionId: answer.knowledge_review_question_id,
            isCorrect: isCorrect,
          });

          // Store selected answer in JSON format for both multiple choice and text input
          const selectedAnswersJson: any = {};
          if (question.question_type === KnowledgeReviewQuestionType.MULTIPLE_CHOICE && knowledgeReviewAnswerId) {
            selectedAnswersJson.knowledge_review_answer_id = knowledgeReviewAnswerId;
          } else if (question.question_type === KnowledgeReviewQuestionType.TEXT_INPUT && answer.answer_text) {
            selectedAnswersJson.answer_text = answer.answer_text;
          }

          const resultData: any = {
            course_id: submitDto.course_id,
            participant_id: participantId,
            knowledge_review_id: submitDto.knowledge_review_id,
            knowledge_review_question_id: answer.knowledge_review_question_id,
            knowledge_review_answer_id: knowledgeReviewAnswerId,
            course_module_id: courseModuleId,
            course_module_page_id: courseModulePageId, // NULL for course-level KR, page ID for embedded quizzes/assignments
            submission_type: submissionType, // COURSE_LEVEL, EMBEDDED_QUIZ, or EMBEDDED_ASSIGNMENT
            is_correct: isCorrect,
            selected_answers: selectedAnswersJson,
          };
          const result = await tx.knowledgeReviewResult.create({
            data: resultData,
          });

          results.push(result);
          if (isCorrect) {
            correctCount++;
          } else {
            incorrectCount++;
          }
        }

        // Calculate score
        const totalQuestions = questions.length;
        const score = totalQuestions > 0 ? (correctCount / totalQuestions) * 100 : 0;

        // Get pass percentage from knowledge review
        const passPercentage = knowledgeReview.pass_percentage ?? null;

        // Check if participant passed
        // Assignments always pass (no pass percentage validation) - only quizzes validate pass percentage
        const passed =
          submissionType === KnowledgeReviewSubmissionType.EMBEDDED_ASSIGNMENT ||
          passPercentage === null ||
          score >= passPercentage;

        let knowledgeReviewParticipantId: number | null = null;

        if (passed) {
          // Create knowledge review participant record only if passed
          const participantData: any = {
            course_id: submitDto.course_id,
            knowledge_review_id: submitDto.knowledge_review_id,
            participant_id: participantId,
            course_module_page_id: courseModulePageId,
            submission_type: submissionType, // COURSE_LEVEL, EMBEDDED_QUIZ, or EMBEDDED_ASSIGNMENT
            review_completion_date: new Date(),
            review_score: score,
            correct_answers_count: correctCount,
            incorrect_answers_count: incorrectCount,
            total_answers_count: totalQuestions,
          };
          const knowledgeReviewParticipant = await tx.knowledgeReviewParticipant.create({
            data: participantData,
          });
          knowledgeReviewParticipantId = knowledgeReviewParticipant.id;
        } else {
          // Failed - delete all results that were just created
          await tx.knowledgeReviewResult.deleteMany({
            where: {
              knowledge_review_id: submitDto.knowledge_review_id,
              participant_id: participantId,
              course_module_page_id: courseModulePageId,
            },
          });
        }

        const result = {
          success: true,
          knowledgeReviewParticipantId: knowledgeReviewParticipantId,
          passed: passed,
          score: score,
          passPercentage: passPercentage,
          questionResults: questionResults, // Include question results for review display
        };

        return result;
      })
      .then(async (result) => {
        // Only emit event if passed (participant record was created)
        if (result.passed && result.knowledgeReviewParticipantId) {
          // Emit event outside transaction
          // Get learningGroupParticipantId from enrollment
          const enrollment = await this.prisma.client.learningGroupParticipant.findFirst({
            where: {
              participant_id: participantId,
              learningGroup: {
                course_id: submitDto.course_id,
              },
            },
          });

          if (enrollment) {
            // Only emit KNOWLEDGE_REVIEW_COMPLETED event for standalone knowledge reviews
            // (course_module_page_id is NULL for standalone KR)
            // Embedded page quizzes should NOT trigger this event
            if (courseModulePageId === null) {
              this.eventEmitter.emit(
                COURSE_PROGRESS_EVENTS.KNOWLEDGE_REVIEW_COMPLETED,
                new KnowledgeReviewCompletedEvent(
                  enrollment.id,
                  participantId,
                  submitDto.course_id,
                  result.knowledgeReviewParticipantId,
                ),
              );
            }
          }
        }

        return result;
      });
  }
}

results matching ""

    No results matching ""