apps/recallassess/recallassess-api/src/api/admin/report-log/services/report-log.service.ts
BNestBaseModuleService
Properties |
|
Methods |
| Async add | ||||||
add(data: any)
|
||||||
|
Override add method to remove foreign key fields before passing to Prisma Prisma only accepts relation objects (participant, course, etc.), not raw foreign keys
Parameters :
Returns :
Promise<any>
|
| Async getDetail | ||||||
getDetail(id: number)
|
||||||
|
Override getDetail to normalize Decimal fields in related entities This prevents DecimalError when Decimal fields are undefined Follows the same pattern as learning-group.service.ts
Parameters :
|
| Async getPdfUrl | ||||||||
getPdfUrl(id: number)
|
||||||||
|
Get PDF URL for a report log Extracts S3 path from the report log title and returns presigned URL
Parameters :
Returns :
Promise<literal type>
Presigned S3 URL for the PDF |
| Protected mediaUtilService |
Type : BNestMediaUtilService
|
Decorators :
@Inject()
|
import { BNestBaseModuleService } from "@bish-nest/core/data/module-service/base-module.service";
import { BNestMediaUtilService } from "@bish-nest/core/services";
import { DetailResponseDataInterface } from "@bish-nest/core/interfaces/detail-response-data.interface";
import { Inject, Injectable, NotFoundException, UnprocessableEntityException } from "@nestjs/common";
import { plainToInstance } from "class-transformer";
@Injectable()
export class ReportLogService extends BNestBaseModuleService {
@Inject() protected mediaUtilService!: BNestMediaUtilService;
/**
* Override add method to remove foreign key fields before passing to Prisma
* Prisma only accepts relation objects (participant, course, etc.), not raw foreign keys
*/
async add(data: any): Promise<any> {
// Create a copy of data without the foreign key fields
// The transformed relation fields are already present
const {
participant_id,
course_id,
assessment_id,
course_group_id,
email_log_id,
...prismaData
} = data;
// Call parent add method with cleaned data
return super.add(prismaData);
}
/**
* Override getDetail to normalize Decimal fields in related entities
* This prevents DecimalError when Decimal fields are undefined
* Follows the same pattern as learning-group.service.ts
*/
async getDetail(id: number): Promise<DetailResponseDataInterface<unknown>> {
const moduleCurrentCfg = this.gVars.moduleCurrentCfg;
// Load report log with all relations
const reportLog = await this.prisma.client.reportLog.findUnique({
where: { id },
include: {
participant: true,
course: true,
assessment: true,
courseGroup: true,
emailLog: true,
userCreatedBy: {
select: {
id: true,
first_name: true,
last_name: true,
},
},
userUpdatedBy: {
select: {
id: true,
first_name: true,
last_name: true,
},
},
},
});
if (!reportLog) {
const msg = "The record you are looking for is not found.";
throw new UnprocessableEntityException(msg);
}
// Keep the raw Prisma object shape and normalize Decimal fields
// Normalize completion_percentage in courseGroup if present
let normalizedCourseGroup = reportLog.courseGroup;
if (reportLog.courseGroup) {
const completionPercentage =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(reportLog.courseGroup as any).completion_percentage != null
? Number((reportLog.courseGroup as any).completion_percentage)
: null;
normalizedCourseGroup = {
...reportLog.courseGroup,
completion_percentage: completionPercentage,
} as any;
// Normalize completion_percentage in learningGroupParticipants if present
if ((reportLog.courseGroup as any).learningGroupParticipants) {
const normalizedParticipants = (
(reportLog.courseGroup as any).learningGroupParticipants || []
).map((participant: any) => ({
...participant,
completion_percentage:
participant.completion_percentage != null
? Number(participant.completion_percentage)
: null,
}));
normalizedCourseGroup = {
...normalizedCourseGroup,
learningGroupParticipants: normalizedParticipants,
} as any;
}
}
// Normalize individual_quotient in assessment if present (through assessmentParticipants)
let normalizedAssessment = reportLog.assessment;
if (reportLog.assessment && (reportLog.assessment as any).assessmentParticipants) {
const normalizedAssessmentParticipants = (
(reportLog.assessment as any).assessmentParticipants || []
).map((ap: any) => ({
...ap,
individual_quotient:
ap.individual_quotient != null ? Number(ap.individual_quotient) : null,
}));
normalizedAssessment = {
...reportLog.assessment,
assessmentParticipants: normalizedAssessmentParticipants,
} as any;
}
// Normalize individual_quotient in participant if present (through assessmentParticipants)
let normalizedParticipant = reportLog.participant;
if (reportLog.participant && (reportLog.participant as any).assessmentParticipants) {
const normalizedParticipantAssessmentParticipants = (
(reportLog.participant as any).assessmentParticipants || []
).map((ap: any) => ({
...ap,
individual_quotient:
ap.individual_quotient != null ? Number(ap.individual_quotient) : null,
}));
normalizedParticipant = {
...reportLog.participant,
assessmentParticipants: normalizedParticipantAssessmentParticipants,
} as any;
}
const sanitizedData: Record<string, unknown> = {
...reportLog,
courseGroup: normalizedCourseGroup,
assessment: normalizedAssessment,
participant: normalizedParticipant,
};
// Transform to DTO for scalar fields (id, name, dates, etc.)
const data = plainToInstance(moduleCurrentCfg.detailDto, sanitizedData);
return this.moduleMethods.getReturnDataForDetail(data);
}
/**
* Get PDF URL for a report log
* Extracts S3 path from the report log title and returns presigned URL
* @param id Report log ID
* @returns Presigned S3 URL for the PDF
*/
async getPdfUrl(id: number): Promise<{ pdfUrl: string }> {
const reportLog = await this.prisma.client.reportLog.findUnique({
where: { id },
});
if (!reportLog) {
throw new NotFoundException(`Report log with ID ${id} not found`);
}
// Extract S3 filename from title (format: "Report Title [filename.pdf]")
const titleMatch = reportLog.title.match(/\[([^\]]+\.pdf)\]/);
if (!titleMatch || !titleMatch[1]) {
throw new NotFoundException(
`PDF filename not found in report log title. The report log may not have a PDF associated with it.`,
);
}
const filename = titleMatch[1];
// Reconstruct the S3 path from the filename
// Filename format: {participantId}-{pre|post}-bat-{timestamp}.pdf
// Path uses course_group_id from the report log plus the filename from the title.
const learningGroupId = reportLog.course_group_id;
if (!learningGroupId) {
throw new NotFoundException(
`Learning group ID not found in report log. Cannot determine S3 path.`,
);
}
// Construct the full S3 path
const s3Path = `private/report-log/${learningGroupId}/document/${filename}`;
// Get presigned URL from media service
const pdfUrl = await this.mediaUtilService.getS3Url(s3Path, false);
return { pdfUrl };
}
}