From d48d85df2f160feeab32f1ef6ea78d1fed13189b Mon Sep 17 00:00:00 2001 From: Victor Jack <67293106+thedon-dev@users.noreply.github.com> Date: Sat, 15 Aug 2026 10:30:45 +0100 Subject: [PATCH] fix(api): allow inviting students to compliance courses without a certificate deadline (#840) * Allow teachers invite students * Greptile comment fix * coderabbit comment fix --- apps/api/src/services/course/compliance.ts | 45 ++++++++++++++----- .../features/course/api/compliance.svelte.ts | 8 ++-- .../features/course/pages/compliance.svelte | 24 ++++------ 3 files changed, 47 insertions(+), 30 deletions(-) diff --git a/apps/api/src/services/course/compliance.ts b/apps/api/src/services/course/compliance.ts index ddaaea41..cfe95556 100644 --- a/apps/api/src/services/course/compliance.ts +++ b/apps/api/src/services/course/compliance.ts @@ -135,16 +135,17 @@ async function getTargetStudentMembers(courseId: string, profileIds?: string[]) return students; } -function resolveComplianceInitialDueDate(course: Awaited>) { +/** + * Resolves the initial compliance cycle due date from the course certificate + * deadline. Returns `null` when no deadline is set — callers must then skip + * record creation (enrollment itself is still allowed). Throws only when a + * deadline is present but malformed. + */ +function getComplianceInitialDueDate(course: Awaited>): string | null { const dueDate = course.certificate?.deadline ?? null; if (!dueDate) { - throw new AppError( - 'Compliance courses require a certificate deadline before learners can be enrolled', - ErrorCodes.VALIDATION_ERROR, - 400, - 'certificate.deadline' - ); + return null; } const parsedDueDate = new Date(dueDate); @@ -211,7 +212,12 @@ async function getComplianceCompletionSnapshot(params: { async function ensureComplianceEnrollmentRecordForLearner(courseId: string, groupMemberId: string, profileId: string) { const course = await getComplianceCourseOrThrow(courseId); - const dueDate = resolveComplianceInitialDueDate(course); + const dueDate = getComplianceInitialDueDate(course); + + if (!dueDate) { + return null; + } + const [existingRecord] = await getLatestComplianceRecordsByProfiles(courseId, [profileId]); if (existingRecord) { @@ -242,11 +248,14 @@ export async function ensureComplianceEnrollmentRecordsForProfiles(courseIds: st for (const courseId of courseIds) { const [course] = await getCourseById(courseId); - if (!course || course.type !== 'COMPLIANCE') { + if (!course || course.type !== 'COMPLIANCE' || !course.compliance) { continue; } - resolveComplianceInitialDueDate(await getComplianceCourseOrThrow(courseId)); + const initialDueDate = getComplianceInitialDueDate(course); + if (!initialDueDate) { + continue; + } const learners = await getStudentCourseMembersForCompliance(courseId, profileIds); for (const learner of learners) { @@ -283,6 +292,14 @@ export async function syncComplianceProgressFromSubmission(courseId: string, gro } const activeRecord = await ensureComplianceEnrollmentRecordForLearner(courseId, groupMemberId, profile.id); + + if (!activeRecord) { + return { + status: 'not_started', + completed: false + }; + } + if (!activeRecord.completedAt && activeRecord.status === 'not_started') { await updateCourseCompletionRecord(activeRecord.id, { status: 'in_progress', @@ -299,6 +316,14 @@ export async function syncComplianceProgressFromSubmission(courseId: string, gro } const latestRecord = await ensureComplianceEnrollmentRecordForLearner(courseId, groupMemberId, profile.id); + + if (!latestRecord) { + return { + status: 'not_started', + completed: false + }; + } + if (latestRecord.completedAt) { return { status: latestRecord.status, diff --git a/apps/dashboard/src/lib/features/course/api/compliance.svelte.ts b/apps/dashboard/src/lib/features/course/api/compliance.svelte.ts index 5592e857..30ca9508 100644 --- a/apps/dashboard/src/lib/features/course/api/compliance.svelte.ts +++ b/apps/dashboard/src/lib/features/course/api/compliance.svelte.ts @@ -49,8 +49,8 @@ export class ComplianceApi extends BaseApiWithErrors { return result?.data; } - async ensureOverview(courseId: string) { - if (this.overviewKey === courseId && this.overview) { + async ensureOverview(courseId: string, refresh = false) { + if (!refresh && this.overviewKey === courseId && this.overview) { return this.overview; } @@ -80,9 +80,9 @@ export class ComplianceApi extends BaseApiWithErrors { return result?.data; } - async ensureLearnerHistory(courseId: string, profileId: string) { + async ensureLearnerHistory(courseId: string, profileId: string, refresh = false) { const requestKey = `${courseId}:${profileId}`; - if (this.learnerHistoryKey === requestKey && this.learnerHistory) { + if (!refresh && this.learnerHistoryKey === requestKey && this.learnerHistory) { return this.learnerHistory; } diff --git a/apps/dashboard/src/lib/features/course/pages/compliance.svelte b/apps/dashboard/src/lib/features/course/pages/compliance.svelte index eb1d8a27..eee5b7c5 100644 --- a/apps/dashboard/src/lib/features/course/pages/compliance.svelte +++ b/apps/dashboard/src/lib/features/course/pages/compliance.svelte @@ -1,4 +1,5 @@