File

apps/recallassess/recallassess-api/src/api/integration/integration.service.ts

Index

Properties
Methods

Constructor

constructor(prisma: BNestPrismaService, browserService: BNestBrowserService, config: ConfigService, reportService: BNestReportService, angularSSR: BNestAngularSSRService, preBatAnalysisService: PreBatAnalysisService, postBatAnalysisService: PostBatAnalysisService, systemLogService: SystemLogService)
Parameters :
Name Type Optional
prisma BNestPrismaService No
browserService BNestBrowserService No
config ConfigService No
reportService BNestReportService No
angularSSR BNestAngularSSRService No
preBatAnalysisService PreBatAnalysisService No
postBatAnalysisService PostBatAnalysisService No
systemLogService SystemLogService No

Methods

Async checkPostBatPdfExists
checkPostBatPdfExists(learningGroupId: number, participantId: number)

Check if Post-BAT PDF exists in S3 Delegates to PostBatAnalysisService

Parameters :
Name Type Optional
learningGroupId number No
participantId number No
Returns : Promise<string>
Async checkPreBatPdfExists
checkPreBatPdfExists(learningGroupId: number, participantId: number)

Check if Pre-BAT PDF exists in S3 Delegates to PreBatAnalysisService

Parameters :
Name Type Optional
learningGroupId number No
participantId number No
Returns : Promise<string>
Async generateAndUploadPostBatPdf
generateAndUploadPostBatPdf(learningGroupId: number, participantId: number, course: literal type, authorization?: string)

Generate POST-BAT PDF and upload to S3 Delegates to PostBatAnalysisService

Parameters :
Name Type Optional
learningGroupId number No
participantId number No
course literal type No
authorization string Yes
Returns : Promise<string>
Async generateAndUploadPreBatPdf
generateAndUploadPreBatPdf(learningGroupId: number, participantId: number, course: literal type, authorization?: string, freshAnalysisData?: string)

Generate PDF and upload to S3 Delegates to PreBatAnalysisService

Parameters :
Name Type Optional
learningGroupId number No
participantId number No
course literal type No
authorization string Yes
freshAnalysisData string Yes
Returns : Promise<string>
Async generatePostBatHtml
generatePostBatHtml(learningGroupId: number, participantId: number, courseCode: string)

Generate POST-BAT HTML preview Delegates to PostBatAnalysisService

Parameters :
Name Type Optional
learningGroupId number No
participantId number No
courseCode string No
Returns : Promise<string>
Async generatePreBatHtml
generatePreBatHtml(learningGroupId: number, participantId: number)

Generate pre-BAT HTML for a participant (preview) Delegates to PreBatAnalysisService

Parameters :
Name Type Optional
learningGroupId number No
participantId number No
Returns : Promise<string>
Async generatePreBatPdfDirect
generatePreBatPdfDirect(learningGroupId: number, participantId: number, course: literal type)

Generate Pre-BAT PDF directly and return buffer (for download) Delegates to PreBatAnalysisService

Parameters :
Name Type Optional
learningGroupId number No
participantId number No
course literal type No
Returns : Promise<Buffer>
Async getCourseForLearningGroup
getCourseForLearningGroup(learningGroupId: number)

Get course information for a learning group

Parameters :
Name Type Optional Description
learningGroupId number No

Learning group ID

Returns : Promise<literal type>
Async getCoursesForIntegration
getCoursesForIntegration()

Get list of published courses for CodeRythm integration. Returns course_code, course_title, and related modules (published course modules).

Returns : Promise<Array<literal type>>
Async getLearningGroupIdForCourse
getLearningGroupIdForCourse(courseId: number)

Get learning group ID for a course

Parameters :
Name Type Optional Description
courseId number No

Course ID

Returns : Promise<number>
Async getPostBatAnalysisData
getPostBatAnalysisData(learningGroupId: number, participantId: number, course: literal type, options?: literal type)

Get Post-BAT analysis data for individual report rendering Delegates to PostBatAnalysisService

Parameters :
Name Type Optional
learningGroupId number No
participantId number No
course literal type No
options literal type Yes
Returns : Promise<literal type>
Async getPreBatAnalysisData
getPreBatAnalysisData(learningGroupId: number, participantId: number, course: literal type, includeChartImage: unknown, freshAnalysisData?: string)

Get Pre-BAT analysis data for individual report rendering Delegates to PreBatAnalysisService

Parameters :
Name Type Optional Default value
learningGroupId number No
participantId number No
course literal type No
includeChartImage unknown No false
freshAnalysisData string Yes
Returns : Promise<literal type>
Async handlePostBatAnalysis
handlePostBatAnalysis(dto: IntegrationPostBatAnalysisDto)

Handle post-bat-analysis request (comparative matrix with module-level data)

Parameters :
Name Type Optional Description
dto IntegrationPostBatAnalysisDto No

Request DTO containing result with metadata and trainees array

Returns : Promise<literal type>

Success response

Async handlePostBatGroupAnalysis
handlePostBatGroupAnalysis(dto: IntegrationPostBatGroupAnalysisDto)

Handle post-bat-group-analysis request (team analysis - POST BAT)

Parameters :
Name Type Optional Description
dto IntegrationPostBatGroupAnalysisDto No

Request DTO containing learning_group_id, course_code, and analysis

Returns : Promise<literal type>

Success response

Async handlePreBatAnalysis
handlePreBatAnalysis(dto: IntegrationPreBatAnalysisDto)

Handle pre-bat-analysis request

Parameters :
Name Type Optional Description
dto IntegrationPreBatAnalysisDto No

Request DTO containing result with metadata and trainees array

Returns : Promise<literal type>

Success response with sample data

Async handlePreBatGroupAnalysis
handlePreBatGroupAnalysis(dto: IntegrationPreBatGroupAnalysisDto)

Handle pre-bat-group-analysis request (team analysis - PRE BAT)

Parameters :
Name Type Optional Description
dto IntegrationPreBatGroupAnalysisDto No

Request DTO containing learning_group_id, course_code, and analysis

Returns : Promise<literal type>

Success response

Properties

Private Readonly logger
Type : unknown
Default value : new Logger(IntegrationService.name)
import {
  BAT_COLOR_HEX,
  BAT_LEVEL_TO_QUOTIENT,
  BAT_LEVEL_TO_SCORE,
  getColorClassFromScore,
  SCORE_COLOR_THRESHOLDS,
  SCORE_TO_METRIC,
} from "@api/shared/constants/assessment-scores.constants";
import { SystemLogService } from "@api/shared/services";
import { BNestReportService } from "@bish-nest/core/admin/modules/report/report.service";
import { BNestBrowserService } from "@bish-nest/core/services/browser.service";
import { BNestPrismaService } from "@bish-nest/core/services/database/prisma/prisma.service";
import { BNestAngularSSRService } from "@bish-nest/core/services/pdf/angular-ssr.service";
import { BadRequestException, Inject, Injectable, Logger, NotFoundException } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { AssessmentType, SystemLogEntityType } from "@prisma/client";
import {
  IntegrationPostBatAnalysisDto,
  IntegrationPostBatGroupAnalysisDto,
  IntegrationPreBatAnalysisDto,
  IntegrationPreBatGroupAnalysisDto,
} from "./dto";
import { PostBatAnalysisService } from "./post-bat-analysis.service";
import { PreBatAnalysisService } from "./pre-bat-analysis.service";

@Injectable()
export class IntegrationService {
  private readonly logger = new Logger(IntegrationService.name);

  constructor(
    private readonly prisma: BNestPrismaService,
    @Inject() private readonly browserService: BNestBrowserService,
    private readonly config: ConfigService,
    @Inject() private readonly reportService: BNestReportService,
    @Inject() private readonly angularSSR: BNestAngularSSRService,
    private readonly preBatAnalysisService: PreBatAnalysisService,
    private readonly postBatAnalysisService: PostBatAnalysisService,
    private readonly systemLogService: SystemLogService,
  ) {}

  /**
   * Handle pre-bat-analysis request
   * @param dto Request DTO containing result with metadata and trainees array
   * @returns Success response with sample data
   */
  async handlePreBatAnalysis(dto: IntegrationPreBatAnalysisDto): Promise<{ success: boolean; message: string }> {
    // Validate DTO structure
    if (!dto.result) {
      throw new BadRequestException('Request body must contain a "result" object');
    }
    if (!dto.result.metadata) {
      throw new BadRequestException('Request body must contain "result.metadata" object');
    }
    if (!dto.result.trainees) {
      throw new BadRequestException('Request body must contain "result.trainees" array');
    }

    const learningGroupId = dto.result.metadata.learning_group_id;
    const trainees = dto.result.trainees;

    this.logger.log(
      `Processing pre-bat-analysis for ${trainees.length} trainee(s) in learning group ${learningGroupId}`,
    );

    // Verify learning group exists
    const learningGroup = await this.prisma.client.learningGroup.findUnique({
      where: { id: learningGroupId },
    });

    if (!learningGroup) {
      throw new NotFoundException(`Learning group with ID ${learningGroupId} not found`);
    }

    // Get course information
    const course = await this.prisma.client.course.findUnique({
      where: { id: learningGroup.course_id },
    });

    if (!course) {
      throw new NotFoundException(`Course not found for learning group ${learningGroupId}`);
    }

    // Get assessment_id from course
    if (!course.assessment_id) {
      throw new NotFoundException(`Course ${course.id} does not have an assessment assigned`);
    }

    // Log full raw CR JSON payload in system_log (IMPORT) for debug/audit
    try {
      await this.systemLogService.logImport(SystemLogEntityType.ASSESSMENT, {
        entity_id: course.assessment_id,
        request_body: dto as unknown as Record<string, unknown>,
      });
    } catch {
      // System logging failure must not break main flow
    }

    // Process each trainee
    const results = [];
    for (const trainee of trainees) {
      const participantId = trainee.participant_id;

      // Verify participant exists
      const participant = await this.prisma.client.participant.findUnique({
        where: { id: participantId },
      });

      if (!participant) {
        this.logger.warn(`Participant with ID ${participantId} not found, skipping`);
        continue;
      }

      // Verify participant is enrolled in this learning group
      const enrollment = await this.prisma.client.learningGroupParticipant.findFirst({
        where: {
          learning_group_id: learningGroupId,
          participant_id: participantId,
        },
      });

      if (!enrollment) {
        this.logger.warn(
          `Participant ${participantId} is not enrolled in learning group ${learningGroupId}, skipping`,
        );
        continue;
      }

      // Check if PRE-BAT assessment is completed for this participant (required before processing analysis)
      const existingPreBat = await this.prisma.client.assessmentParticipant.findFirst({
        where: {
          assessment_id: course.assessment_id,
          participant_id: participantId,
          learning_group_id: learningGroupId,
          assessment_type: AssessmentType.PRE_BAT,
        },
      });

      if (!existingPreBat?.assessment_completion_date) {
        const errorMsg = `PRE-BAT assessment not completed for participant ${participantId}. Participant must complete the assessment before analysis can be processed.`;
        this.logger.error(errorMsg);
        throw new BadRequestException(errorMsg);
      }

      // Find or create AssessmentParticipant record and update ai_analysis
      await this.prisma.client.assessmentParticipant.upsert({
        where: {
          assessment_id_participant_id_assessment_type: {
            assessment_id: course.assessment_id,
            participant_id: participantId,
            assessment_type: AssessmentType.PRE_BAT,
          },
        },
        update: {
          ai_analysis: trainee.pre_report,
        },
        create: {
          course_id: course.id,
          assessment_id: course.assessment_id,
          participant_id: participantId,
          learning_group_id: learningGroupId,
          assessment_type: AssessmentType.PRE_BAT,
          ai_analysis: trainee.pre_report,
        },
      });

      this.logger.log(
        `Pre-BAT analysis saved for participant ${participantId} in assessment ${course.assessment_id}`,
      );

      // Generate PDF and upload to S3, then create report log with S3 path
      // Pass fresh analysis data directly to ensure PDF uses latest data
      let pdfS3Path: string | null = null;
      try {
        pdfS3Path = await this.preBatAnalysisService.generateAndUploadPreBatPdf(
          learningGroupId,
          participantId,
          course,
          undefined,
          trainee.pre_report,
        );
        this.logger.log(`Pre-BAT PDF generated and uploaded to S3: ${pdfS3Path}`);
      } catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        this.logger.error(
          `Failed to generate/upload Pre-BAT PDF for participant ${participantId}: ${errorMessage}`,
        );
        // Don't fail the entire request if PDF generation fails
      }

      // Create report log entry for each analysis received
      // Each time analysis is received, create a new log entry to track all report generations
      // Include S3 path in the title so we can track which PDF belongs to which log
      try {
        const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, -5); // Format: 2025-12-18T23-30-45
        const courseName = course.title || course.course_code || "Course";
        // Extract just the filename from S3 path for cleaner title
        const s3FileName = pdfS3Path ? pdfS3Path.split("/").pop() : null;
        const reportTitle = pdfS3Path
          ? `Pre-BAT Analysis Report - ${courseName} [${s3FileName}]`
          : `Pre-BAT Analysis Report - ${courseName} [${timestamp}] [PDF Generation Failed]`;

        await this.prisma.client.reportLog.create({
          data: {
            participant_id: participantId,
            course_id: course.id,
            assessment_id: course.assessment_id,
            course_group_id: learningGroupId,
            title: reportTitle,
            sent_status: false,
          },
        });
        this.logger.log(
          `Report log created for Pre-BAT analysis - participant ${participantId}${pdfS3Path ? ` (S3: ${pdfS3Path})` : " (PDF generation failed)"}`,
        );
      } catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        this.logger.warn(
          `Failed to create report log for Pre-BAT analysis - participant ${participantId}: ${errorMessage}`,
        );
        // Don't fail the entire request if report log creation fails
      }

      results.push({ participantId, success: true });
    }

    // Check if any participants were processed
    if (results.length === 0) {
      throw new BadRequestException(
        "No participants were processed. Please verify that participants exist, are enrolled in the learning group, and trainees data is valid.",
      );
    }

    return {
      success: true,
      message: `Pre-BAT analysis processed successfully for ${results.length} participant(s)`,
    };
  }

  /**
   * Handle post-bat-analysis request (comparative matrix with module-level data)
   * @param dto Request DTO containing result with metadata and trainees array
   * @returns Success response
   */
  async handlePostBatAnalysis(dto: IntegrationPostBatAnalysisDto): Promise<{ success: boolean; message: string }> {
    const learningGroupId = dto.result.metadata.learning_group_id;
    const courseCode = dto.result.metadata.course_code;
    const trainees = dto.result.trainees;

    this.logger.log(
      `Processing post-bat-analysis for ${trainees.length} trainee(s) in learning group ${learningGroupId}`,
    );

    // Verify learning group exists
    const learningGroup = await this.prisma.client.learningGroup.findUnique({
      where: { id: learningGroupId },
    });

    if (!learningGroup) {
      throw new NotFoundException(`Learning group with ID ${learningGroupId} not found`);
    }

    // Verify course code matches
    const course = await this.prisma.client.course.findUnique({
      where: { id: learningGroup.course_id },
    });

    if (!course) {
      throw new NotFoundException(`Course not found for learning group ${learningGroupId}`);
    }

    // Verify course code matches the provided course_code
    if (course.course_code !== courseCode) {
      throw new NotFoundException(
        `Course code mismatch. Expected ${course.course_code}, but received ${courseCode}`,
      );
    }

    // Get assessment_id from course
    if (!course.assessment_id) {
      throw new NotFoundException(`Course ${course.id} does not have an assessment assigned`);
    }

    // Resolve module codes from payload to CourseModule ids (no course-scoped mapping)
    // Only needed when trainees use legacy modules format
    const allModuleCodes = new Set<string>();
    for (const trainee of trainees) {
      if (trainee.modules && typeof trainee.modules === "object") {
        for (const code of Object.keys(trainee.modules)) {
          allModuleCodes.add(code);
        }
      }
    }
    const moduleCodeToIdMap = new Map<string, number>();
    if (allModuleCodes.size > 0) {
      const modules = await this.prisma.client.courseModule.findMany({
        where: { course_module_code: { in: [...allModuleCodes] } },
        select: { id: true, course_module_code: true },
      });
      for (const m of modules) {
        if (m.course_module_code) {
          moduleCodeToIdMap.set(m.course_module_code, m.id);
        }
      }
    }

    // Log full raw CR JSON payload in system_log (IMPORT) for debug/audit
    try {
      await this.systemLogService.logImport(SystemLogEntityType.ASSESSMENT, {
        entity_id: course.assessment_id,
        request_body: dto as unknown as Record<string, unknown>,
      });
    } catch {
      // System logging failure must not break main flow
    }

    // Process each trainee
    const results = [];
    for (const trainee of trainees) {
      const participantId = trainee.participant_id;

      // Verify participant exists
      const participant = await this.prisma.client.participant.findUnique({
        where: { id: participantId },
      });

      if (!participant) {
        this.logger.warn(`Participant with ID ${participantId} not found, skipping`);
        continue;
      }

      // Verify participant is enrolled in this learning group
      const enrollment = await this.prisma.client.learningGroupParticipant.findFirst({
        where: {
          learning_group_id: learningGroupId,
          participant_id: participantId,
        },
      });

      if (!enrollment) {
        this.logger.warn(
          `Participant ${participantId} is not enrolled in learning group ${learningGroupId}, skipping`,
        );
        continue;
      }

      // Check if PRE-BAT assessment is completed (required before POST-BAT)
      const existingPreBat = await this.prisma.client.assessmentParticipant.findFirst({
        where: {
          assessment_id: course.assessment_id,
          participant_id: participantId,
          learning_group_id: learningGroupId,
          assessment_type: AssessmentType.PRE_BAT,
        },
      });

      if (!existingPreBat?.assessment_completion_date) {
        const errorMsg = `PRE-BAT assessment not completed for participant ${participantId}. Participant must complete PRE-BAT before POST-BAT analysis can be processed.`;
        this.logger.error(errorMsg);
        throw new BadRequestException(errorMsg);
      }

      // Check if POST-BAT assessment is completed for this participant (required before processing analysis)
      const existingPostBat = await this.prisma.client.assessmentParticipant.findFirst({
        where: {
          assessment_id: course.assessment_id,
          participant_id: participantId,
          learning_group_id: learningGroupId,
          assessment_type: AssessmentType.POST_BAT,
        },
      });

      if (!existingPostBat?.assessment_completion_date) {
        const errorMsg = `POST-BAT assessment not completed for participant ${participantId}. Participant must complete the assessment before analysis can be processed.`;
        this.logger.error(errorMsg);
        throw new BadRequestException(errorMsg);
      }

      // Store the complete analysis in AssessmentParticipant (post_ai_analysis column for POST_BAT)
      await this.prisma.client.assessmentParticipant.upsert({
        where: {
          assessment_id_participant_id_assessment_type: {
            assessment_id: course.assessment_id,
            participant_id: participantId,
            assessment_type: AssessmentType.POST_BAT,
          },
        },
        update: {
          post_ai_analysis: JSON.stringify(trainee),
        } as any,
        create: {
          course_id: course.id,
          assessment_id: course.assessment_id,
          participant_id: participantId,
          learning_group_id: learningGroupId,
          assessment_type: AssessmentType.POST_BAT,
          post_ai_analysis: JSON.stringify(trainee),
        } as any,
      });

      const hasOverallSummary =
        typeof (trainee as { overall_summary?: string }).overall_summary === "string" &&
        ((trainee as { overall_summary?: string }).overall_summary ?? "").trim().length > 0;
      const hasModules =
        trainee.modules && typeof trainee.modules === "object" && Object.keys(trainee.modules).length > 0;

      if (!hasOverallSummary && !hasModules) {
        throw new BadRequestException(
          `Each trainee must have either overall_summary or modules. Participant ${participantId} has neither.`,
        );
      }

      // New format: one summary in post_ai_analysis only. Skip per-topic AssessmentResult updates.
      if (hasOverallSummary) {
        this.logger.log(
          `Post-BAT analysis saved for participant ${participantId} (overall summary in post_ai_analysis)`,
        );
        results.push({ participantId, modulesProcessed: 0, success: true });

        let pdfS3Path: string | null = null;
        try {
          pdfS3Path = await this.postBatAnalysisService.generateAndUploadPostBatPdf(
            learningGroupId,
            participantId,
            course,
          );
          this.logger.log(`Post-BAT PDF generated and uploaded to S3: ${pdfS3Path}`);
        } catch (error) {
          const errorMessage = error instanceof Error ? error.message : String(error);
          this.logger.error(
            `Failed to generate/upload Post-BAT PDF for participant ${participantId}: ${errorMessage}`,
          );
        }

        try {
          const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, -5);
          const courseName = course.title || course.course_code || "Course";
          const s3FileName = pdfS3Path ? pdfS3Path.split("/").pop() : null;
          const reportTitle = pdfS3Path
            ? `Post-BAT Analysis Report - ${courseName} [${s3FileName}]`
            : `Post-BAT Analysis Report - ${courseName} [${timestamp}] [PDF Generation Failed]`;

          await this.prisma.client.reportLog.create({
            data: {
              participant_id: participantId,
              course_id: course.id,
              assessment_id: course.assessment_id,
              course_group_id: learningGroupId,
              title: reportTitle,
              sent_status: false,
            },
          });
        } catch (error) {
          const errorMessage = error instanceof Error ? error.message : String(error);
          this.logger.warn(
            `Failed to create report log for Post-BAT analysis - participant ${participantId}: ${errorMessage}`,
          );
        }
        continue;
      }

      // Find POST BAT participant record (we'll need its ID for FK)
      const postBatParticipant = await this.prisma.client.assessmentParticipant.findFirst({
        where: {
          assessment_id: course.assessment_id,
          participant_id: participantId,
          learning_group_id: learningGroupId,
          assessment_type: AssessmentType.POST_BAT,
        },
      });

      if (!postBatParticipant) {
        this.logger.warn(
          `POST BAT AssessmentParticipant not found for participant ${participantId}, skipping module updates`,
        );
        continue;
      }

      // Process each module in the trainee's data
      let modulesProcessed = 0;
      const traineeModules = trainee.modules ?? {};
      for (const [moduleCode, moduleData] of Object.entries(traineeModules)) {
        const moduleId = moduleCodeToIdMap.get(moduleCode);

        if (!moduleId) {
          this.logger.warn(`Module code ${moduleCode} not found, skipping`);
          continue;
        }

        // Update assessment results for this module with the analysis data
        // Use assessment_participant_id FK instead of timestamp filtering
        const updateResult = await this.prisma.client.assessmentResult.updateMany({
          where: {
            assessment_participant_id: postBatParticipant.id, // ✅ Use FK instead of timestamps
            course_id: course.id,
            assessment_id: course.assessment_id,
            participant_id: participantId,
            course_module_id: moduleId,
          },
          data: {
            learning_path: moduleData.learning_path,
            additional_learning_path: moduleData.additional_learning_path || null,
            references:
              moduleData.references && moduleData.references.length > 0
                ? JSON.parse(JSON.stringify(moduleData.references))
                : null,
          },
        });

        if (updateResult.count > 0) {
          this.logger.log(
            `Updated ${updateResult.count} assessment result(s) for module ${moduleCode} (${moduleId}) - participant ${participantId}`,
          );
          modulesProcessed++;
        } else {
          this.logger.warn(
            `No assessment results found for module ${moduleCode} (${moduleId}) - participant ${participantId} with assessment_participant_id ${postBatParticipant.id}`,
          );
        }
      }

      // Check if any modules were processed for this participant
      if (modulesProcessed === 0) {
        const errorMsg = `No modules were updated for participant ${participantId}. Please verify that module codes match the course modules and assessment results exist.`;
        this.logger.error(errorMsg);
        throw new BadRequestException(errorMsg);
      }

      this.logger.log(
        `Post-BAT analysis saved for participant ${participantId}: ${modulesProcessed} modules processed`,
      );

      // Generate PDF and upload to S3, then create report log with S3 path
      let pdfS3Path: string | null = null;
      try {
        pdfS3Path = await this.postBatAnalysisService.generateAndUploadPostBatPdf(
          learningGroupId,
          participantId,
          course,
        );
        this.logger.log(`Post-BAT PDF generated and uploaded to S3: ${pdfS3Path}`);
      } catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        this.logger.error(
          `Failed to generate/upload Post-BAT PDF for participant ${participantId}: ${errorMessage}`,
        );
        // Don't fail the entire request if PDF generation fails
      }

      // Create report log entry for each analysis received
      // Each time analysis is received, create a new log entry to track all report generations
      // Include S3 path in the title so we can track which PDF belongs to which log
      try {
        const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, -5); // Format: 2025-12-18T23-30-45
        const courseName = course.title || course.course_code || "Course";
        // Extract just the filename from S3 path for cleaner title
        const s3FileName = pdfS3Path ? pdfS3Path.split("/").pop() : null;
        const reportTitle = pdfS3Path
          ? `Post-BAT Analysis Report - ${courseName} [${s3FileName}]`
          : `Post-BAT Analysis Report - ${courseName} [${timestamp}] [PDF Generation Failed]`;

        await this.prisma.client.reportLog.create({
          data: {
            participant_id: participantId,
            course_id: course.id,
            assessment_id: course.assessment_id,
            course_group_id: learningGroupId,
            title: reportTitle,
            sent_status: false,
          },
        });
        this.logger.log(
          `Report log created for Post-BAT analysis - participant ${participantId}${pdfS3Path ? ` (S3: ${pdfS3Path})` : " (PDF generation failed)"}`,
        );
      } catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        this.logger.warn(
          `Failed to create report log for Post-BAT analysis - participant ${participantId}: ${errorMessage}`,
        );
        // Don't fail the entire request if report log creation fails
      }

      results.push({ participantId, modulesProcessed, success: true });
    }

    // Check if any participants were processed
    if (results.length === 0) {
      throw new BadRequestException(
        "No participants were processed. Please verify that participants exist, are enrolled in the learning group, and trainees data is valid.",
      );
    }

    return {
      success: true,
      message: `Post-BAT analysis processed successfully for ${results.length} participant(s)`,
    };
  }

  /**
   * Handle pre-bat-group-analysis request (team analysis - PRE BAT)
   * @param dto Request DTO containing learning_group_id, course_code, and analysis
   * @returns Success response
   */
  async handlePreBatGroupAnalysis(
    dto: IntegrationPreBatGroupAnalysisDto,
  ): Promise<{ success: boolean; message: string }> {
    this.logger.log(`Processing pre-bat-group-analysis for learning group ${dto.learning_group_id}`);

    // Verify learning group exists
    const learningGroup = await this.prisma.client.learningGroup.findUnique({
      where: { id: dto.learning_group_id },
    });

    if (!learningGroup) {
      throw new NotFoundException(`Learning group with ID ${dto.learning_group_id} not found`);
    }

    // Verify course code matches
    const course = await this.prisma.client.course.findUnique({
      where: { id: learningGroup.course_id },
    });

    if (!course) {
      throw new NotFoundException(`Course not found for learning group ${dto.learning_group_id}`);
    }

    // TODO: Store the analysis data or process it as needed
    // This is a placeholder for the actual integration logic
    this.logger.log(
      `Pre-BAT group analysis (team analysis) received for learning group ${dto.learning_group_id}: ${dto.analysis.substring(0, 100)}...`,
    );

    return {
      success: true,
      message: "Pre-BAT group analysis (team analysis) processed successfully",
    };
  }

  /**
   * Handle post-bat-group-analysis request (team analysis - POST BAT)
   * @param dto Request DTO containing learning_group_id, course_code, and analysis
   * @returns Success response
   */
  async handlePostBatGroupAnalysis(
    dto: IntegrationPostBatGroupAnalysisDto,
  ): Promise<{ success: boolean; message: string }> {
    this.logger.log(`Processing post-bat-group-analysis for learning group ${dto.learning_group_id}`);

    // Verify learning group exists
    const learningGroup = await this.prisma.client.learningGroup.findUnique({
      where: { id: dto.learning_group_id },
    });

    if (!learningGroup) {
      throw new NotFoundException(`Learning group with ID ${dto.learning_group_id} not found`);
    }

    // Verify course code matches
    const course = await this.prisma.client.course.findUnique({
      where: { id: learningGroup.course_id },
    });

    if (!course) {
      throw new NotFoundException(`Course not found for learning group ${dto.learning_group_id}`);
    }

    // TODO: Store the analysis data or process it as needed
    // This is a placeholder for the actual integration logic
    this.logger.log(
      `Post-BAT group analysis (team analysis) received for learning group ${dto.learning_group_id}: ${dto.analysis.substring(0, 100)}...`,
    );

    return {
      success: true,
      message: "Post-BAT group analysis (team analysis) processed successfully",
    };
  }

  /**
   * Generate pre-BAT HTML for a participant (preview)
   * Delegates to PreBatAnalysisService
   */
  async generatePreBatHtml(learningGroupId: number, participantId: number): Promise<string> {
    return this.preBatAnalysisService.generatePreBatHtml(learningGroupId, participantId);
  }

  /**
   * Generate Pre-BAT PDF directly and return buffer (for download)
   * Delegates to PreBatAnalysisService
   */
  async generatePreBatPdfDirect(
    learningGroupId: number,
    participantId: number,
    course: { id: number; assessment_id: number | null; title: string | null },
  ): Promise<Buffer> {
    return this.preBatAnalysisService.generatePreBatPdfDirect(learningGroupId, participantId, course);
  }

  /**
   * Check if Pre-BAT PDF exists in S3
   * Delegates to PreBatAnalysisService
   */
  async checkPreBatPdfExists(learningGroupId: number, participantId: number): Promise<string> {
    return this.preBatAnalysisService.checkPreBatPdfExists(learningGroupId, participantId);
  }

  /**
   * Generate PDF and upload to S3
   * Delegates to PreBatAnalysisService
   */
  async generateAndUploadPreBatPdf(
    learningGroupId: number,
    participantId: number,
    course: { id: number; assessment_id: number | null; title: string | null },
    authorization?: string,
    freshAnalysisData?: string,
  ): Promise<string> {
    return this.preBatAnalysisService.generateAndUploadPreBatPdf(
      learningGroupId,
      participantId,
      course,
      authorization,
      freshAnalysisData,
    );
  }

  /**
   * Get Pre-BAT analysis data for individual report rendering
   * Delegates to PreBatAnalysisService
   */
  async getPreBatAnalysisData(
    learningGroupId: number,
    participantId: number,
    course: { id: number; assessment_id: number | null; title: string | null },
    includeChartImage = false,
    freshAnalysisData?: string,
  ): Promise<{
    courseTitle: string;
    participantName: string;
    skills: string;
    ipq: number;
    preTrainingResults: string;
    preReport: string;
    chartImage?: string | null;
  }> {
    return this.preBatAnalysisService.getPreBatAnalysisData(
      learningGroupId,
      participantId,
      course,
      includeChartImage,
      freshAnalysisData,
    );
  }

  // Removed: calculateSkillsFromResults - moved to PreBatAnalysisService
  // Removed: calculateIpqFromResults - moved to PreBatAnalysisService
  // Removed: calculatePreTrainingResults - moved to PreBatAnalysisService
  // Removed: renderSkillChart - moved to BatChartService
  // Removed: getColorHex - moved to BatChartService

  /**
   * Get Post-BAT analysis data for individual report rendering
   * Delegates to PostBatAnalysisService
   */
  async getPostBatAnalysisData(
    learningGroupId: number,
    participantId: number,
    course: { id: number; assessment_id: number | null; title: string | null },
    options?: { includeChartImages?: boolean },
  ): Promise<{
    courseTitle: string;
    participantName: string;
    preIpq: number;
    postIpq: number;
    moduleComparisons: string;
    overallSummary?: string | null;
    preChartImage?: string | null;
    postChartImage?: string | null;
  }> {
    return this.postBatAnalysisService.getPostBatAnalysisData(learningGroupId, participantId, course, options);
  }

  /**
   * Generate POST-BAT HTML preview
   * Delegates to PostBatAnalysisService
   */
  async generatePostBatHtml(learningGroupId: number, participantId: number, courseCode: string): Promise<string> {
    return this.postBatAnalysisService.generatePostBatHtml(learningGroupId, participantId, courseCode);
  }

  /**
   * Check if Post-BAT PDF exists in S3
   * Delegates to PostBatAnalysisService
   */
  async checkPostBatPdfExists(learningGroupId: number, participantId: number): Promise<string> {
    return this.postBatAnalysisService.checkPostBatPdfExists(learningGroupId, participantId);
  }

  /**
   * Generate POST-BAT PDF and upload to S3
   * Delegates to PostBatAnalysisService
   */
  async generateAndUploadPostBatPdf(
    learningGroupId: number,
    participantId: number,
    course: { id: number; assessment_id: number | null; title: string | null },
    authorization?: string,
  ): Promise<string> {
    return this.postBatAnalysisService.generateAndUploadPostBatPdf(
      learningGroupId,
      participantId,
      course,
      authorization,
    );
  }

  /**
   * Get learning group ID for a course
   * @param courseId Course ID
   */
  async getLearningGroupIdForCourse(courseId: number): Promise<number> {
    const learningGroup = await this.prisma.client.learningGroup.findFirst({
      where: { course_id: courseId },
      select: { id: true },
    });

    if (!learningGroup) {
      throw new NotFoundException(`No learning group found for course ${courseId}`);
    }

    return learningGroup.id;
  }

  /**
   * Get course information for a learning group
   * @param learningGroupId Learning group ID
   */
  async getCourseForLearningGroup(
    learningGroupId: number,
  ): Promise<{ id: number; assessment_id: number | null; title: string | null }> {
    const learningGroup = await this.prisma.client.learningGroup.findUnique({
      where: { id: learningGroupId },
      select: {
        course_id: true,
        course: {
          select: {
            id: true,
            title: true,
            assessment_id: true,
          },
        },
      },
    });

    if (!learningGroup || !learningGroup.course) {
      throw new NotFoundException(`Course not found for learning group ${learningGroupId}`);
    }

    return {
      id: learningGroup.course.id,
      assessment_id: learningGroup.course.assessment_id,
      title: learningGroup.course.title,
    };
  }

  /**
   * Get list of published courses for CodeRythm integration.
   * Returns course_code, course_title, and related modules (published course modules).
   */
  async getCoursesForIntegration(): Promise<
    Array<{
      course_id: number;
      course_code: string;
      course_title: string;
      modules: Array<{ module_id: number; module_code: string; module_title: string }>;
    }>
  > {
    const courses = await this.prisma.client.course.findMany({
      where: {
        is_published: true,
      },
      orderBy: [{ sort_order: "asc" }, { title: "asc" }],
      include: {
        courseModulePages: {
          where: { is_published: true },
          orderBy: { sort_order: "asc" },
          select: { courseModule: true },
        },
      },
    });

    return courses.map((course) => {
      const seenModuleIds = new Set<number>();
      const modules: Array<{ module_id: number; module_code: string; module_title: string }> = [];
      for (const page of course.courseModulePages) {
        const mod = page.courseModule;
        if (mod?.is_published && !seenModuleIds.has(mod.id)) {
          seenModuleIds.add(mod.id);
          modules.push({
            module_id: mod.id,
            module_code: mod.course_module_code,
            module_title: mod.title,
          });
        }
      }
      return {
        course_id: course.id,
        course_code: course.course_code,
        course_title: course.title,
        modules,
      };
    });
  }
}

results matching ""

    No results matching ""