feat: certification rules, content sanitization, handle draft course updates and reset content, better redis logging

This commit is contained in:
Rotimi Best
2026-03-30 08:24:55 +01:00
parent ff2ad59377
commit 5a8f50f67b
131 changed files with 5643 additions and 2134 deletions
+89
View File
@@ -0,0 +1,89 @@
import { getDashboardBaseUrl } from '@api/config/dashboard-url';
import {
getCourseProgress as getCourseProgressQuery,
type TCourseCertificationRow
} from '@cio/db/queries/course/course';
import { setMemberCertificateEarned, setMemberCertificationEmailSent } from '@cio/db/queries/course/people';
import { getProfileById } from '@cio/db/queries/auth';
import { buildEmailFromName, sendEmail } from '@cio/email';
export function calcCourseProgressPercent(params: {
lessonsCompleted: number;
totalLessons: number;
exercisesCompleted: number;
exercisesCount: number;
}): number {
const totalItems = params.totalLessons + params.exercisesCount;
if (totalItems === 0) return 0;
const completedItems = params.lessonsCompleted + params.exercisesCompleted;
return Math.round((completedItems / totalItems) * 100);
}
export function isBeforeOrEqualDeadline(deadlineIso: string | null | undefined, now: Date): boolean {
if (!deadlineIso) return true;
const deadlineDate = new Date(deadlineIso);
if (Number.isNaN(deadlineDate.getTime())) return true;
return now.getTime() <= deadlineDate.getTime();
}
/**
* Persists certificate earned time, then sends the completion email and records
* {@link setMemberCertificationEmailSent}, without blocking the HTTP handler.
* Re-fetches progress so concurrent jobs can skip work already done.
*/
export function scheduleCertificationCompletionWork(params: {
courseId: string;
profileId: string;
groupMemberId: string;
courseRow: TCourseCertificationRow;
earnedAt: string;
}): void {
const { courseId, profileId, groupMemberId, courseRow, earnedAt } = params;
void (async () => {
try {
let latest = await getCourseProgressQuery(courseId, profileId);
if (!latest.certificateEarnedAt) {
await setMemberCertificateEarned(groupMemberId, earnedAt);
latest = await getCourseProgressQuery(courseId, profileId);
}
if (latest.certificationEmailSentAt) {
return;
}
const profile = await getProfileById(profileId);
const studentEmail = profile?.email;
if (!studentEmail) {
return;
}
const base = getDashboardBaseUrl(courseRow.orgSiteName ?? undefined);
const certificateUrl = `${base}/courses/${courseId}/certificates`;
await sendEmail('studentCourseCompletion', {
to: studentEmail,
fields: {
orgName: courseRow.orgName,
courseName: courseRow.title,
studentName: profile?.fullname || studentEmail,
certificateUrl,
customMessage: courseRow.certificate?.emailMessage ?? null
},
from: buildEmailFromName(`${courseRow.orgName} (via ClassroomIO.com)`)
});
await setMemberCertificationEmailSent(groupMemberId, new Date().toISOString());
} catch (error) {
console.error('scheduleCertificationCompletionWork: failed:', error);
}
})();
}