apps/recallassess/recallassess-api/src/api/integration/integration.service.ts
Properties |
|
Methods |
|
constructor(prisma: BNestPrismaService, browserService: BNestBrowserService, config: ConfigService, reportService: BNestReportService, angularSSR: BNestAngularSSRService, preBatAnalysisService: PreBatAnalysisService, postBatAnalysisService: PostBatAnalysisService, systemLogService: SystemLogService)
|
|||||||||||||||||||||||||||
|
Parameters :
|
| Async checkPostBatPdfExists |
checkPostBatPdfExists(learningGroupId: number, participantId: number)
|
|
Check if Post-BAT PDF exists in S3 Delegates to PostBatAnalysisService
Returns :
Promise<string>
|
| Async checkPreBatPdfExists |
checkPreBatPdfExists(learningGroupId: number, participantId: number)
|
|
Check if Pre-BAT PDF exists in S3 Delegates to PreBatAnalysisService
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 :
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 :
Returns :
Promise<string>
|
| Async generatePostBatHtml | ||||||||||||
generatePostBatHtml(learningGroupId: number, participantId: number, courseCode: string)
|
||||||||||||
|
Generate POST-BAT HTML preview Delegates to PostBatAnalysisService
Parameters :
Returns :
Promise<string>
|
| Async generatePreBatHtml |
generatePreBatHtml(learningGroupId: number, participantId: number)
|
|
Generate pre-BAT HTML for a participant (preview) Delegates to PreBatAnalysisService
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 :
Returns :
Promise<Buffer>
|
| Async getCourseForLearningGroup | ||||||||
getCourseForLearningGroup(learningGroupId: number)
|
||||||||
|
Get course information for a learning group
Parameters :
Returns :
Promise<literal type>
|
| Async getLearningGroupIdForCourse | ||||||||
getLearningGroupIdForCourse(courseId: number)
|
||||||||
|
Get learning group ID for a course
Parameters :
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 :
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 :
Returns :
Promise<literal type>
|
| Async handlePostBatAnalysis | ||||||||
handlePostBatAnalysis(dto: IntegrationPostBatAnalysisDto)
|
||||||||
|
Handle post-bat-analysis request (comparative matrix with module-level data)
Parameters :
Returns :
Promise<literal type>
Success response |
| Async handlePostBatGroupAnalysis | ||||||||
handlePostBatGroupAnalysis(dto: IntegrationPostBatGroupAnalysisDto)
|
||||||||
|
Handle post-bat-group-analysis request (team analysis - POST BAT)
Parameters :
Returns :
Promise<literal type>
Success response |
| Async handlePreBatAnalysis | ||||||||
handlePreBatAnalysis(dto: IntegrationPreBatAnalysisDto)
|
||||||||
|
Handle pre-bat-analysis request
Parameters :
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 :
Returns :
Promise<literal type>
Success response |
| 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,
};
});
}
}