// Phase 5B — combines an enrollment row with real curriculum/progress/achievement state into // one "program status" view: access/launch eligibility, stage/percent progress, and a // deterministic next-activity (resume) target. Shared by the student self-paced routes and the // admin program/enrollment routes so both surfaces show identical information computed one way, // not two independently-maintained views that could drift. const { deriveEnrollmentAccess, maybeCompleteEnrollment } = require('./enrollment'); const { computeCurriculumState } = require('./curriculum-rules'); async function buildProgramStatus(deps, enrollmentIn) { const { pool, curriculum, loadProgress } = deps; let enrollment = enrollmentIn; // Opportunistic, idempotent completion check (§10) — safe to run on every read; the // underlying UPDATE's `WHERE status = 'active'` guard makes a repeat call a no-op once an // enrollment is already completed, so this never double-fires or races itself. if (enrollment.status === 'active') { const completed = await maybeCompleteEnrollment(pool, enrollment.student_id, enrollment.program_id); if (completed.length) enrollment = { ...enrollment, ...completed[0] }; } const access = deriveEnrollmentAccess(enrollment); // Only one curriculum path exists today (curriculum.js) — enrollment.curriculum_path is the // snapshot that would let a second path be resolved here later without touching this // function's shape; see docs/PHASE5B_SELF_PACED_ENROLLMENT.md §5. const path = curriculum.loadCurriculumPath(); const progress = await loadProgress(pool, enrollment.student_id); const capstoneStatus = enrollment.status === 'completed' ? 'completed' : undefined; const state = path ? computeCurriculumState(path, progress, capstoneStatus) : null; let nextActivity = null; if (state) { const currentStage = state.stages.find((s) => s.id === state.current_stage); if (currentStage?.next_scenario) { nextActivity = { type: 'scenario', scenario_id: currentStage.next_scenario, stage_id: currentStage.id }; } else if (state.capstone && state.capstone.status === 'unlocked') { nextActivity = { type: 'capstone', assessment_definition_id: state.capstone.assessment_definition_id }; } } return { enrollment_id: enrollment.enrollment_id, program_id: enrollment.program_id, program_name: enrollment.program_name || null, program_family: enrollment.program_family || null, status: enrollment.status, effective_status: access.effective_status, can_launch: access.can_launch, blocked_reason: access.reason, delivery_mode: enrollment.delivery_mode, starts_at: enrollment.starts_at, access_ends_at: enrollment.access_ends_at, enrolled_at: enrollment.enrolled_at, activated_at: enrollment.activated_at, completed_at: enrollment.completed_at, cancelled_at: enrollment.cancelled_at, progress: state ? { percent_complete: state.percent_complete, current_stage: state.current_stage, stages: state.stages, capstone: state.capstone } : null, next_activity: nextActivity, }; } module.exports = { buildProgramStatus };