feat: certificates, landing pages, ai tutor, website update, compliance

This commit is contained in:
Rotimi Best
2026-05-18 03:47:22 +01:00
parent c31ad63955
commit ac3a2c14a7
644 changed files with 56050 additions and 5439 deletions
+738
View File
@@ -0,0 +1,738 @@
import {
and,
course,
courseCompletionRecord,
courseSection,
db,
eq,
exercise,
group,
groupmember,
inArray,
lesson,
organizationmember,
profile,
user
} from '@db/drizzle';
import type {
TNewCourse,
TNewCourseCompletionRecord,
TNewCourseSection,
TNewExercise,
TNewGroup,
TNewGroupmember,
TNewLesson,
TNewProfile
} from '@db/types';
interface SeedComplianceArgs {
enterpriseOrgId: string;
}
// ---------- Fixed IDs (stable across runs) ----------
const HIPAA_GROUP_ID = '7e000000-1000-4000-8000-000000000001';
const SOC2_GROUP_ID = '7e000000-1000-4000-8000-000000000002';
const HIPAA_COURSE_ID = '7e000000-2000-4000-8000-000000000001';
const SOC2_COURSE_ID = '7e000000-2000-4000-8000-000000000002';
const HIPAA_SECTION_ID = '7e000000-3000-4000-8000-000000000001';
const SOC2_SECTION_ID = '7e000000-3000-4000-8000-000000000002';
const HIPAA_LESSON_IDS = ['7e000000-4000-4000-8000-000000000001', '7e000000-4000-4000-8000-000000000002'] as const;
const SOC2_LESSON_IDS = ['7e000000-4000-4000-8000-000000000003', '7e000000-4000-4000-8000-000000000004'] as const;
const HIPAA_EXERCISE_ID = '7e000000-5000-4000-8000-000000000001';
const SOC2_EXERCISE_ID = '7e000000-5000-4000-8000-000000000002';
// 20 stable learner profile UUIDs
const LEARNER_PROFILES: Array<{ id: string; fullname: string; username: string; email: string; avatarUrl: string }> = [
{
id: '7e000000-9000-4000-8000-000000000001',
fullname: 'Ada Lovelace',
username: 'ada.lovelace',
email: '[email protected]'
},
{
id: '7e000000-9000-4000-8000-000000000002',
fullname: 'Grace Hopper',
username: 'grace.hopper',
email: '[email protected]'
},
{
id: '7e000000-9000-4000-8000-000000000003',
fullname: 'Linus Torvalds',
username: 'linus.torvalds',
email: '[email protected]'
},
{
id: '7e000000-9000-4000-8000-000000000004',
fullname: 'Margaret Hamilton',
username: 'margaret.hamilton',
email: '[email protected]'
},
{
id: '7e000000-9000-4000-8000-000000000005',
fullname: 'Alan Turing',
username: 'alan.turing',
email: '[email protected]'
},
{
id: '7e000000-9000-4000-8000-000000000006',
fullname: 'Katherine Johnson',
username: 'katherine.johnson',
email: '[email protected]'
},
{
id: '7e000000-9000-4000-8000-000000000007',
fullname: 'Dennis Ritchie',
username: 'dennis.ritchie',
email: '[email protected]'
},
{
id: '7e000000-9000-4000-8000-000000000008',
fullname: 'Barbara Liskov',
username: 'barbara.liskov',
email: '[email protected]'
},
{
id: '7e000000-9000-4000-8000-000000000009',
fullname: 'Donald Knuth',
username: 'donald.knuth',
email: '[email protected]'
},
{
id: '7e000000-9000-4000-8000-000000000010',
fullname: 'Radia Perlman',
username: 'radia.perlman',
email: '[email protected]'
},
{
id: '7e000000-9000-4000-8000-000000000011',
fullname: 'Tim Berners-Lee',
username: 'tim.bernerslee',
email: '[email protected]'
},
{
id: '7e000000-9000-4000-8000-000000000012',
fullname: 'Sophie Wilson',
username: 'sophie.wilson',
email: '[email protected]'
},
{
id: '7e000000-9000-4000-8000-000000000013',
fullname: 'Anita Borg',
username: 'anita.borg',
email: '[email protected]'
},
{
id: '7e000000-9000-4000-8000-000000000014',
fullname: 'Brendan Eich',
username: 'brendan.eich',
email: '[email protected]'
},
{
id: '7e000000-9000-4000-8000-000000000015',
fullname: 'Jeannette Wing',
username: 'jeannette.wing',
email: '[email protected]'
},
{
id: '7e000000-9000-4000-8000-000000000016',
fullname: 'Bjarne Stroustrup',
username: 'bjarne.stroustrup',
email: '[email protected]'
},
{
id: '7e000000-9000-4000-8000-000000000017',
fullname: 'Hedy Lamarr',
username: 'hedy.lamarr',
email: '[email protected]'
},
{
id: '7e000000-9000-4000-8000-000000000018',
fullname: 'James Gosling',
username: 'james.gosling',
email: '[email protected]'
},
{
id: '7e000000-9000-4000-8000-000000000019',
fullname: 'Frances Allen',
username: 'frances.allen',
email: '[email protected]'
},
{
id: '7e000000-9000-4000-8000-000000000020',
fullname: 'Vint Cerf',
username: 'vint.cerf',
email: '[email protected]'
}
].map((p) => ({
...p,
avatarUrl: `https://api.dicebear.com/9.x/notionists/svg?seed=${encodeURIComponent(p.username)}`
}));
// ---------- Helpers ----------
const DAY_MS = 24 * 60 * 60 * 1000;
function isoDaysFromNow(now: Date, days: number): string {
return new Date(now.getTime() + days * DAY_MS).toISOString();
}
type RecordPlan = {
status:
| 'compliant'
| 'expiring_soon'
| 'in_grace_period'
| 'non_compliant'
| 'in_progress'
| 'not_started'
| 'waived'
| 'no_record';
};
// Per-course learner-index → status plan. Indices reference LEARNER_PROFILES (0..19).
const HIPAA_STATUS_PLAN: ReadonlyArray<RecordPlan['status']> = [
// 9 compliant
'compliant',
'compliant',
'compliant',
'compliant',
'compliant',
'compliant',
'compliant',
'compliant',
'compliant',
// 2 expiring_soon
'expiring_soon',
'expiring_soon',
// 1 in_grace_period
'in_grace_period',
// 2 non_compliant
'non_compliant',
'non_compliant',
// 2 in_progress
'in_progress',
'in_progress',
// 2 not_started
'not_started',
'not_started',
// 1 waived
'waived',
// 1 no_record (no row inserted)
'no_record'
];
const SOC2_STATUS_PLAN: ReadonlyArray<RecordPlan['status']> = [
// 7 compliant
'compliant',
'compliant',
'compliant',
'compliant',
'compliant',
'compliant',
'compliant',
// 2 expiring_soon
'expiring_soon',
'expiring_soon',
// 2 in_grace_period
'in_grace_period',
'in_grace_period',
// 3 non_compliant
'non_compliant',
'non_compliant',
'non_compliant',
// 2 in_progress
'in_progress',
'in_progress',
// 2 not_started
'not_started',
'not_started',
// 1 waived
'waived',
// 1 no_record
'no_record'
];
function buildCompletionRecord({
courseId,
groupMemberId,
profileId,
status,
now,
retakeIntervalMonths,
waiverAdminProfileId
}: {
courseId: string;
groupMemberId: string;
profileId: string;
status: RecordPlan['status'];
now: Date;
retakeIntervalMonths: number;
waiverAdminProfileId: string | null;
}): TNewCourseCompletionRecord | null {
if (status === 'no_record') return null;
const validityDays = retakeIntervalMonths * 30;
switch (status) {
case 'compliant': {
// Completed ~ between (validityDays - 90) and (validityDays - 30) days ago,
// so validUntil is well in the future (>30 days away).
const completedDaysAgo = Math.max(30, Math.floor(validityDays / 3));
const completedAt = isoDaysFromNow(now, -completedDaysAgo);
const validUntil = isoDaysFromNow(now, validityDays - completedDaysAgo);
return {
courseId,
groupMemberId,
profileId,
cycleNumber: 1,
status,
dueDate: completedAt,
startedAt: isoDaysFromNow(now, -completedDaysAgo - 2),
completedAt,
validUntil,
score: 88,
attempts: 1,
timeSpentMinutes: 42
};
}
case 'expiring_soon': {
// completedAt ~ (validityDays - 7) days ago → validUntil 7 days from now.
const completedDaysAgo = validityDays - 7;
const completedAt = isoDaysFromNow(now, -completedDaysAgo);
const validUntil = isoDaysFromNow(now, 7);
return {
courseId,
groupMemberId,
profileId,
cycleNumber: 1,
status,
dueDate: completedAt,
startedAt: isoDaysFromNow(now, -completedDaysAgo - 1),
completedAt,
validUntil,
score: 91,
attempts: 1,
timeSpentMinutes: 38
};
}
case 'in_grace_period': {
// dueDate 3 days in the past, never completed.
return {
courseId,
groupMemberId,
profileId,
cycleNumber: 1,
status,
dueDate: isoDaysFromNow(now, -3),
startedAt: isoDaysFromNow(now, -10),
attempts: 0
};
}
case 'non_compliant': {
// dueDate 30 days in the past, never completed.
return {
courseId,
groupMemberId,
profileId,
cycleNumber: 1,
status,
dueDate: isoDaysFromNow(now, -30),
startedAt: isoDaysFromNow(now, -20),
attempts: 0
};
}
case 'in_progress': {
return {
courseId,
groupMemberId,
profileId,
cycleNumber: 1,
status,
dueDate: isoDaysFromNow(now, 21),
startedAt: isoDaysFromNow(now, -2),
attempts: 0,
timeSpentMinutes: 12
};
}
case 'not_started': {
return {
courseId,
groupMemberId,
profileId,
cycleNumber: 1,
status,
dueDate: isoDaysFromNow(now, 21),
attempts: 0
};
}
case 'waived': {
return {
courseId,
groupMemberId,
profileId,
cycleNumber: 1,
status,
dueDate: isoDaysFromNow(now, 14),
waivedBy: waiverAdminProfileId ?? profileId,
waiverReason: 'On approved leave through end of cycle',
waiverExpiresAt: isoDaysFromNow(now, 90)
};
}
}
}
// ---------- Main seed ----------
export async function seedCompliance({ enterpriseOrgId }: SeedComplianceArgs) {
const now = new Date();
// 1a. Users (profile.id FKs to user.id)
const profileIds = LEARNER_PROFILES.map((p) => p.id);
const existingUsers = await db.select({ id: user.id }).from(user).where(inArray(user.id, profileIds));
const existingUserIds = new Set(existingUsers.map((u) => u.id));
const usersToInsert = LEARNER_PROFILES.filter((p) => !existingUserIds.has(p.id)).map((p) => ({
id: p.id,
name: p.fullname,
email: p.email,
emailVerified: true,
image: null,
role: null,
banned: false,
isAnonymous: false
}));
if (usersToInsert.length > 0) {
await db.insert(user).values(usersToInsert);
console.log(` ✓ Inserted ${usersToInsert.length} learner user(s)`);
} else {
console.log(' ✓ Learner users already exist, skipping');
}
// 1b. Profiles
const existingProfiles = await db.select({ id: profile.id }).from(profile).where(inArray(profile.id, profileIds));
const existingProfileIds = new Set(existingProfiles.map((p) => p.id));
const profilesToInsert: TNewProfile[] = LEARNER_PROFILES.filter((p) => !existingProfileIds.has(p.id)).map((p) => ({
id: p.id,
fullname: p.fullname,
username: p.username,
email: p.email,
avatarUrl: p.avatarUrl,
canAddCourse: false,
isEmailVerified: true
}));
if (profilesToInsert.length > 0) {
await db.insert(profile).values(profilesToInsert);
console.log(` ✓ Inserted ${profilesToInsert.length} learner profile(s)`);
} else {
console.log(' ✓ Learner profiles already exist, skipping');
}
// 2. Groups
const existingGroups = await db
.select({ id: group.id })
.from(group)
.where(inArray(group.id, [HIPAA_GROUP_ID, SOC2_GROUP_ID]));
const existingGroupIds = new Set(existingGroups.map((g) => g.id));
const groupsToInsert: TNewGroup[] = [
{
id: HIPAA_GROUP_ID,
name: 'HIPAA Awareness 2026',
description: 'Mandatory annual HIPAA awareness refresher for all Coursera Test staff handling PHI.',
organizationId: enterpriseOrgId
},
{
id: SOC2_GROUP_ID,
name: 'SOC 2 Security Basics',
description: 'Twice-yearly SOC 2 security fundamentals — required for engineering and operations.',
organizationId: enterpriseOrgId
}
].filter((g) => !existingGroupIds.has(g.id));
if (groupsToInsert.length > 0) {
await db.insert(group).values(groupsToInsert);
console.log(` ✓ Inserted ${groupsToInsert.length} compliance group(s)`);
} else {
console.log(' ✓ Compliance groups already exist, skipping');
}
// 3. Courses (COMPLIANCE type)
const existingCourses = await db
.select({ id: course.id })
.from(course)
.where(inArray(course.id, [HIPAA_COURSE_ID, SOC2_COURSE_ID]));
const existingCourseIds = new Set(existingCourses.map((c) => c.id));
const coursesToInsert: TNewCourse[] = [
{
id: HIPAA_COURSE_ID,
title: 'HIPAA Awareness 2026',
description:
'Annual HIPAA refresher covering PHI handling, breach reporting, and minimum-necessary access for everyone who touches patient data.',
overview: '<p>Annual HIPAA refresher covering PHI handling, breach reporting, and minimum-necessary access.</p>',
groupId: HIPAA_GROUP_ID,
isTemplate: false,
logo: 'https://images.unsplash.com/photo-1551601651-2a8555f1a136?auto=format&fit=crop&w=600&q=70',
slug: 'hipaa-awareness-2026',
metadata: {
goals: '',
grading: false,
description: '',
requirements: '',
lessonDownload: false,
allowNewStudent: false
},
cost: 0,
currency: 'USD',
isPublished: true,
certificate: { isDownloadable: true, theme: 'professional' },
compliance: {
retakeIntervalMonths: 12,
gracePeriodDays: 14,
reminderDaysBefore: [30, 14, 7, 1],
isMandatory: true,
framework: 'HIPAA',
passingScore: 80
},
status: 'ACTIVE',
type: 'COMPLIANCE'
},
{
id: SOC2_COURSE_ID,
title: 'SOC 2 Security Basics',
description:
'Core SOC 2 controls — password hygiene, phishing, data classification, incident response. Runs every six months for engineering and operations.',
overview: '<p>Twice-yearly SOC 2 security fundamentals. Required for engineering, IT, and operations.</p>',
groupId: SOC2_GROUP_ID,
isTemplate: false,
logo: 'https://images.unsplash.com/photo-1614064641938-3bbee52942c7?auto=format&fit=crop&w=600&q=70',
slug: 'soc2-security-basics',
metadata: {
goals: '',
grading: true,
description: '',
requirements: '',
lessonDownload: false,
allowNewStudent: false
},
cost: 0,
currency: 'USD',
isPublished: true,
certificate: { isDownloadable: true, theme: 'professional' },
compliance: {
retakeIntervalMonths: 6,
gracePeriodDays: 7,
reminderDaysBefore: [14, 7, 1],
isMandatory: true,
framework: 'ISO',
passingScore: 85
},
status: 'ACTIVE',
type: 'COMPLIANCE'
}
].filter((c) => !existingCourseIds.has(c.id!));
if (coursesToInsert.length > 0) {
await db.insert(course).values(coursesToInsert);
console.log(` ✓ Inserted ${coursesToInsert.length} compliance course(s)`);
} else {
console.log(' ✓ Compliance courses already exist, skipping');
}
// 4. Sections + lessons + exercises
const sectionsToInsert: TNewCourseSection[] = [
{ id: HIPAA_SECTION_ID, courseId: HIPAA_COURSE_ID, title: 'Compliance Module', order: 0 },
{ id: SOC2_SECTION_ID, courseId: SOC2_COURSE_ID, title: 'Security Fundamentals', order: 0 }
];
const existingSections = await db
.select({ id: courseSection.id })
.from(courseSection)
.where(inArray(courseSection.id, [HIPAA_SECTION_ID, SOC2_SECTION_ID]));
const existingSectionIds = new Set(existingSections.map((s) => s.id));
const newSections = sectionsToInsert.filter((s) => !existingSectionIds.has(s.id!));
if (newSections.length > 0) {
await db.insert(courseSection).values(newSections);
console.log(` ✓ Inserted ${newSections.length} compliance section(s)`);
} else {
console.log(' ✓ Compliance sections already exist, skipping');
}
const lessonsPlan: TNewLesson[] = [
{
id: HIPAA_LESSON_IDS[0],
courseId: HIPAA_COURSE_ID,
sectionId: HIPAA_SECTION_ID,
title: 'What HIPAA actually requires of you',
note: '<p>Plain-English summary of the privacy rule, security rule, and breach notification obligations.</p>',
isUnlocked: true
},
{
id: HIPAA_LESSON_IDS[1],
courseId: HIPAA_COURSE_ID,
sectionId: HIPAA_SECTION_ID,
title: 'Handling PHI: the everyday rules',
note: '<p>Minimum necessary access, secure messaging, faxing rules, and what to do if you spot a breach.</p>',
isUnlocked: false
},
{
id: SOC2_LESSON_IDS[0],
courseId: SOC2_COURSE_ID,
sectionId: SOC2_SECTION_ID,
title: 'Passwords, MFA, and not getting phished',
note: '<p>Strong-credential hygiene + recognizing the four most common phishing patterns we see at Coursera Test.</p>',
isUnlocked: true
},
{
id: SOC2_LESSON_IDS[1],
courseId: SOC2_COURSE_ID,
sectionId: SOC2_SECTION_ID,
title: 'Data classification & incident response',
note: '<p>What counts as confidential, who to call when something goes wrong, and how we record an incident.</p>',
isUnlocked: false
}
];
const existingLessons = await db
.select({ id: lesson.id })
.from(lesson)
.where(
inArray(
lesson.id,
lessonsPlan.map((l) => l.id!)
)
);
const existingLessonIds = new Set(existingLessons.map((l) => l.id));
const newLessons = lessonsPlan.filter((l) => !existingLessonIds.has(l.id!));
if (newLessons.length > 0) {
await db.insert(lesson).values(newLessons);
console.log(` ✓ Inserted ${newLessons.length} compliance lesson(s)`);
} else {
console.log(' ✓ Compliance lessons already exist, skipping');
}
const exercisesPlan: TNewExercise[] = [
{
id: HIPAA_EXERCISE_ID,
title: 'HIPAA: end-of-course quiz',
description: '<p>Pass this short quiz to mark the course complete.</p>',
lessonId: HIPAA_LESSON_IDS[1],
sectionId: HIPAA_SECTION_ID
},
{
id: SOC2_EXERCISE_ID,
title: 'SOC 2: end-of-course quiz',
description: '<p>Pass this short quiz to mark the course complete.</p>',
lessonId: SOC2_LESSON_IDS[1],
sectionId: SOC2_SECTION_ID
}
];
const existingExercises = await db
.select({ id: exercise.id })
.from(exercise)
.where(inArray(exercise.id, [HIPAA_EXERCISE_ID, SOC2_EXERCISE_ID]));
const existingExerciseIds = new Set(existingExercises.map((e) => e.id));
const newExercises = exercisesPlan.filter((e) => !existingExerciseIds.has(e.id!));
if (newExercises.length > 0) {
await db.insert(exercise).values(newExercises);
console.log(` ✓ Inserted ${newExercises.length} compliance exercise(s)`);
} else {
console.log(' ✓ Compliance exercises already exist, skipping');
}
// 5. Groupmembers (20 learners × 2 groups)
const desiredMembers: Array<{ groupId: string; profileId: string }> = [];
for (const groupId of [HIPAA_GROUP_ID, SOC2_GROUP_ID]) {
for (const p of LEARNER_PROFILES) {
desiredMembers.push({ groupId, profileId: p.id });
}
}
const existingGroupMembers = await db
.select({ id: groupmember.id, groupId: groupmember.groupId, profileId: groupmember.profileId })
.from(groupmember)
.where(inArray(groupmember.groupId, [HIPAA_GROUP_ID, SOC2_GROUP_ID]));
const existingMemberKey = new Set(existingGroupMembers.map((m) => `${m.groupId}-${m.profileId}`));
const newMembers: TNewGroupmember[] = desiredMembers
.filter((m) => !existingMemberKey.has(`${m.groupId}-${m.profileId}`))
.map((m) => ({
groupId: m.groupId,
profileId: m.profileId,
roleId: 3 // STUDENT
}));
if (newMembers.length > 0) {
await db.insert(groupmember).values(newMembers);
console.log(` ✓ Inserted ${newMembers.length} compliance groupmember(s)`);
} else {
console.log(' ✓ Compliance groupmembers already exist, skipping');
}
// Refetch groupmembers so we can look up the groupMember.id for each (groupId, profileId)
const allMembers = await db
.select({ id: groupmember.id, groupId: groupmember.groupId, profileId: groupmember.profileId })
.from(groupmember)
.where(inArray(groupmember.groupId, [HIPAA_GROUP_ID, SOC2_GROUP_ID]));
const memberLookup = new Map<string, string>();
for (const m of allMembers) {
if (m.profileId) memberLookup.set(`${m.groupId}-${m.profileId}`, m.id);
}
// 6. Resolve a waiver-admin profileId (the org's first admin)
const orgAdmins = await db
.select({ profileId: organizationmember.profileId })
.from(organizationmember)
.where(and(eq(organizationmember.organizationId, enterpriseOrgId), eq(organizationmember.roleId, 1)));
const waiverAdminProfileId = orgAdmins.find((a) => a.profileId)?.profileId ?? null;
// 7. course_completion_record rows
const recordsPlan: Array<{
courseId: string;
status: RecordPlan['status'];
profileIdx: number;
retakeIntervalMonths: number;
}> = [];
HIPAA_STATUS_PLAN.forEach((status, idx) =>
recordsPlan.push({ courseId: HIPAA_COURSE_ID, status, profileIdx: idx, retakeIntervalMonths: 12 })
);
SOC2_STATUS_PLAN.forEach((status, idx) =>
recordsPlan.push({ courseId: SOC2_COURSE_ID, status, profileIdx: idx, retakeIntervalMonths: 6 })
);
const existingRecords = await db
.select({
courseId: courseCompletionRecord.courseId,
profileId: courseCompletionRecord.profileId,
cycleNumber: courseCompletionRecord.cycleNumber
})
.from(courseCompletionRecord)
.where(inArray(courseCompletionRecord.courseId, [HIPAA_COURSE_ID, SOC2_COURSE_ID]));
const existingRecordKey = new Set(existingRecords.map((r) => `${r.courseId}-${r.profileId}-${r.cycleNumber}`));
const recordsToInsert: TNewCourseCompletionRecord[] = [];
for (const plan of recordsPlan) {
const learner = LEARNER_PROFILES[plan.profileIdx];
if (!learner) continue;
const groupId = plan.courseId === HIPAA_COURSE_ID ? HIPAA_GROUP_ID : SOC2_GROUP_ID;
const groupMemberId = memberLookup.get(`${groupId}-${learner.id}`);
if (!groupMemberId) continue;
const key = `${plan.courseId}-${learner.id}-1`;
if (existingRecordKey.has(key)) continue;
const row = buildCompletionRecord({
courseId: plan.courseId,
groupMemberId,
profileId: learner.id,
status: plan.status,
now,
retakeIntervalMonths: plan.retakeIntervalMonths,
waiverAdminProfileId
});
if (row) recordsToInsert.push(row);
}
if (recordsToInsert.length > 0) {
await db.insert(courseCompletionRecord).values(recordsToInsert);
console.log(` ✓ Inserted ${recordsToInsert.length} course_completion_record row(s)`);
} else {
console.log(' ✓ course_completion_record rows already exist, skipping');
}
}