feat(cohorts): rename Programs to Cohorts with data migration (#694)

* feat(cohorts): rename Programs to Cohorts with data migration

- Add cohort database tables and Drizzle migration alongside legacy program tables
- Introduce migrate-programs-to-cohorts script to copy existing program data safely
- Rename API routes (/cohort), services, queries, validation, and error codes
- Rename dashboard feature module, routes (/cohorts), navigation, and translations
- Update docs, emails, and internal compliance cron endpoints for cohort terminology
- Keep legacy program tables in place for rollback until explicitly removed

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(db): restore migrate:answer-data script alongside cohort migration

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(db): cast program enums when migrating data to cohort tables

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(db): truncate cohort tables before program-to-cohort copy

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(db): report accurate row counts in programs-to-cohorts migration

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
This commit is contained in:
Rotimi Best
2026-07-02 12:10:31 +01:00
committed by GitHub
co-authored by Rotimi Best Cursor Agent
parent fe9f8c441f
commit ec61bfe7d9
115 changed files with 19703 additions and 6046 deletions
@@ -4,12 +4,12 @@ import { defineEmail } from '../send';
import { getDefaultTemplate } from '../templates';
import { ZEmailBranding } from '../core/branding';
export const programGoalReminderEmail = defineEmail({
id: 'programGoalReminder',
subject: 'Reminder: a program goal is due soon',
export const cohortGoalReminderEmail = defineEmail({
id: 'cohortGoalReminder',
subject: 'Reminder: a cohort goal is due soon',
schema: z.object({
orgName: z.string().min(1),
programName: z.string().min(1),
cohortName: z.string().min(1),
goalTitle: z.string().min(1),
daysUntilDue: z.number().int(),
completedCount: z.number().int(),
@@ -29,7 +29,7 @@ export const programGoalReminderEmail = defineEmail({
const content = `
<p>Hi there,</p>
<p>This is a reminder that the goal <strong>${fields.goalTitle}</strong> in your program <strong>${fields.programName}</strong> at ${fields.orgName} needs your attention.</p>
<p>This is a reminder that the goal <strong>${fields.goalTitle}</strong> in your program <strong>${fields.cohortName}</strong> at ${fields.orgName} needs your attention.</p>
${dueLine}
<p>Your progress so far: <strong>${progress}</strong>.</p>
<p><a href="${fields.loginUrl}">Open the LMS</a> to keep going.</p>
+6 -6
View File
@@ -20,8 +20,8 @@ export * from './teacher-student-buy-request';
export * from './student-prove-payment';
export * from './teacher-course-welcome';
export * from './student-org-invite';
export * from './student-program-welcome';
export * from './program-goal-reminder';
export * from './student-cohort-welcome';
export * from './cohort-goal-reminder';
export * from './quiz-assigned';
export * from './session';
export * from './submission-received';
@@ -38,8 +38,8 @@ import type { studentCourseCompletionEmail } from './student-course-completion';
import type { studentCourseWelcomeEmail } from './student-course-welcome';
import type { studentProvePaymentEmail } from './student-prove-payment';
import type { studentOrgInviteEmail } from './student-org-invite';
import type { studentProgramWelcomeEmail } from './student-program-welcome';
import type { programGoalReminderEmail } from './program-goal-reminder';
import type { studentCohortWelcomeEmail } from './student-cohort-welcome';
import type { cohortGoalReminderEmail } from './cohort-goal-reminder';
import type { quizAssignedEmail } from './quiz-assigned';
import type { sessionReminderEmail, sessionUpdatedEmail } from './session';
import type { submissionReceivedEmail } from './submission-received';
@@ -72,8 +72,8 @@ export type EmailSchemas = {
studentProvePayment: typeof studentProvePaymentEmail.template.schema;
teacherCourseWelcome: typeof teacherCourseWelcomeEmail.template.schema;
studentOrgInvite: typeof studentOrgInviteEmail.template.schema;
studentProgramWelcome: typeof studentProgramWelcomeEmail.template.schema;
programGoalReminder: typeof programGoalReminderEmail.template.schema;
studentCohortWelcome: typeof studentCohortWelcomeEmail.template.schema;
cohortGoalReminder: typeof cohortGoalReminderEmail.template.schema;
quizAssigned: typeof quizAssignedEmail.template.schema;
sessionReminder: typeof sessionReminderEmail.template.schema;
sessionUpdated: typeof sessionUpdatedEmail.template.schema;
@@ -4,20 +4,20 @@ import { defineEmail } from '../send';
import { getDefaultTemplate } from '../templates';
import { ZEmailBranding } from '../core/branding';
export const studentProgramWelcomeEmail = defineEmail({
id: 'studentProgramWelcome',
subject: 'You have access to a program — sign in to get started',
export const studentCohortWelcomeEmail = defineEmail({
id: 'studentCohortWelcome',
subject: 'You have access to a cohort — sign in to get started',
schema: z.object({
orgName: z.string().min(1),
programName: z.string().min(1),
cohortName: z.string().min(1),
loginUrl: z.string().min(1),
branding: ZEmailBranding
}),
render: (fields) => {
const content = `
<p>Hi there,</p>
<p>You now have access to <strong>${fields.programName}</strong> in <strong>${fields.orgName}</strong>.</p>
<p><a href="${fields.loginUrl}">Sign in to the LMS</a> to open the program and start learning.</p>
<p>You now have access to <strong>${fields.cohortName}</strong> in <strong>${fields.orgName}</strong>.</p>
<p><a href="${fields.loginUrl}">Sign in to the LMS</a> to open the cohort and start learning.</p>
<p>If you run into any issues, reach out to your instructor(s).</p>
<p>Cheers,</p>
<p>${fields.orgName}</p>
+2 -2
View File
@@ -6,7 +6,7 @@ export const EMAIL_IDS = [
'newsfeedComment',
'newsfeedPost',
'onPasswordReset',
'programGoalReminder',
'cohortGoalReminder',
'quizAssigned',
'sessionReminder',
'sessionUpdated',
@@ -16,7 +16,7 @@ export const EMAIL_IDS = [
'studentCourseCompletion',
'studentCourseWelcome',
'studentOrgInvite',
'studentProgramWelcome',
'studentCohortWelcome',
'studentProvePayment',
'teacherCourseWelcome',
'teacherStudentBuyRequest',