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
+25 -36
View File
@@ -5,7 +5,8 @@ import type {
TImportAudienceMembers
} from '@cio/utils/validation/organization';
import { addGroupMembers, enrollUsersInCourseGroups, getExistingGroupMembers } from '@cio/db/queries/group';
import { buildEmailFromName, sendEmail } from '@cio/email';
import { buildEmailFromName } from '@cio/email';
import { enqueueTransactionalEmail } from '@api/services/jobs';
import { addProgramMember, getExistingProgramMembers, getProgramsByOrg } from '@cio/db/queries/program';
import {
createOrganizationInvite,
@@ -202,18 +203,19 @@ async function enrollAudienceStudentProfilesInCourses(
.map(async (p) => {
const email = profileEmailMap.get(p.profileId)!;
try {
await sendEmail('studentCourseWelcome', {
await enqueueTransactionalEmail('studentCourseWelcome', {
to: email,
fields: {
orgName: organization.name,
courseName: courseTitleByGroupId.get(p.groupId) || 'Course',
loginUrl
},
from: buildEmailFromName(`${organization.name} (via ClassroomIO.com)`)
from: buildEmailFromName(`${organization.name} (via ClassroomIO.com)`),
idempotencyKey: `audience-course-welcome:${p.groupId}:${p.profileId}`
});
emailsSent++;
} catch (emailError) {
console.error(`enrollAudienceStudentProfilesInCourses email error for ${email}:`, emailError);
console.error(`enrollAudienceStudentProfilesInCourses enqueue error for ${email}:`, emailError);
}
});
@@ -283,18 +285,19 @@ async function enrollAudienceStudentProfilesInPrograms(
const email = profileEmailMap.get(pair.profileId)!;
try {
await sendEmail('studentProgramWelcome', {
await enqueueTransactionalEmail('studentProgramWelcome', {
to: email,
fields: {
orgName: organization.name,
programName: programNameById.get(pair.programId) || 'Program',
loginUrl
},
from: buildEmailFromName(`${organization.name} (via ClassroomIO.com)`)
from: buildEmailFromName(`${organization.name} (via ClassroomIO.com)`),
idempotencyKey: `audience-program-welcome:${pair.programId}:${pair.profileId}`
});
emailsSent++;
} catch (emailError) {
console.error(`enrollAudienceStudentProfilesInPrograms email error for ${email}:`, emailError);
console.error(`enrollAudienceStudentProfilesInPrograms enqueue error for ${email}:`, emailError);
}
})
);
@@ -385,7 +388,7 @@ async function createStudentOrgInvitesAndSendEmails(input: {
const token = tokenByEmail.get(email)!;
try {
const inviteLink = buildInviteLink(token);
const responses = await sendEmail('studentOrgInvite', {
await enqueueTransactionalEmail('studentOrgInvite', {
to: email,
fields: {
email,
@@ -394,26 +397,16 @@ async function createStudentOrgInvitesAndSendEmails(input: {
expiresAt: getExpiryLabel(expiresAt),
courseNames: accessNamesLabel
},
from: buildEmailFromName(`${organization.name} (via ClassroomIO.com)`)
from: buildEmailFromName(`${organization.name} (via ClassroomIO.com)`),
idempotencyKey: `student-org-invite:${invite.id}`
});
const allSuccessful = responses.every((r) => r.success);
if (allSuccessful) {
return {
inviteId: invite.id,
email,
success: true as const,
error: undefined as string | undefined
};
}
// Optimistic — see comment in services/organization/invite.ts.
return {
inviteId: invite.id,
email,
success: false as const,
error: responses
.filter((r) => !r.success)
.map((r) => r.error || 'Unknown')
.join('; ')
success: true as const,
error: undefined as string | undefined
};
} catch (emailError) {
const message = emailError instanceof Error ? emailError.message : 'Unknown email error';
@@ -685,7 +678,7 @@ export async function resendAudienceInvite(orgId: string, data: TAudienceInviteB
let emailSent = false;
try {
const inviteLink = buildInviteLink(token);
const responses = await sendEmail('studentOrgInvite', {
await enqueueTransactionalEmail('studentOrgInvite', {
to: emailToUse,
fields: {
email: emailToUse,
@@ -694,27 +687,23 @@ export async function resendAudienceInvite(orgId: string, data: TAudienceInviteB
expiresAt: getExpiryLabel(expiresAt),
courseNames: accessNamesLabel
},
from: buildEmailFromName(`${organization.name} (via ClassroomIO.com)`)
from: buildEmailFromName(`${organization.name} (via ClassroomIO.com)`),
idempotencyKey: `student-org-invite:${invite.id}`
});
const allSuccessful = responses.every((r) => r.success);
emailSent = allSuccessful;
emailSent = true;
// Optimistic EMAIL_SENT — worker handles retries; final failure flips
// email_delivery to `failed` for operator follow-up.
await createOrganizationInviteAudits([
{
inviteId: invite.id,
organizationId: orgId,
eventType: allSuccessful ? 'EMAIL_SENT' : 'EMAIL_FAILED',
eventType: 'EMAIL_SENT',
actorProfileId: invitedByProfileId,
targetEmail: emailToUse,
ipAddress: null,
userAgent: null,
metadata: allSuccessful
? {}
: {
error: responses
.filter((r) => !r.success)
.map((r) => r.error || 'Unknown')
.join('; ')
}
metadata: {}
}
]);
} catch (emailError) {
+8 -19
View File
@@ -27,7 +27,7 @@ import { db, type DbOrTxClient } from '@cio/db/drizzle';
import { getDashboardBaseUrl } from '@api/config/dashboard-url';
import { parseCourseIdsFromInviteMetadata, parseProgramIdsFromInviteMetadata } from '@api/utils/org';
import { markUserAndProfileEmailVerified } from '@cio/db/queries/auth/profile';
import { sendEmail } from '@cio/email';
import { enqueueTransactionalEmail } from '@api/services/jobs';
import { ensureComplianceEnrollmentRecordsForProfiles } from '../course/compliance';
type OrganizationInviteStatus = 'ACTIVE' | 'EXPIRED' | 'REVOKED' | 'ACCEPTED';
@@ -244,7 +244,7 @@ export async function inviteTeamMembers(orgId: string, emails: string[], roleId:
const inviteLink = buildInviteLink(token);
try {
const responses = await sendEmail('inviteTeacher', {
await enqueueTransactionalEmail('inviteTeacher', {
to: email,
fields: {
email,
@@ -253,25 +253,14 @@ export async function inviteTeamMembers(orgId: string, emails: string[], roleId:
roleName,
expiresAt: getExpiryLabel(expiresAt),
inviteLink
}
},
idempotencyKey: `org-invite-teacher:${invite.id}`
});
const allSuccessful = responses.every((response) => response.success);
if (!allSuccessful) {
const message = responses
.filter((response) => !response.success)
.map((response) => response.error || 'Unknown email error')
.join('; ');
await recordOrganizationInviteAudit(invite.id, orgId, 'EMAIL_FAILED', {
actorProfileId: invitedByProfileId,
targetEmail: email,
metadata: { error: message }
});
continue;
}
// Record EMAIL_SENT optimistically — the worker retries up to 5 times,
// and a final failure flips the email_delivery row to `failed` for
// operator follow-up. EMAIL_FAILED here only means the enqueue itself
// failed (DB outbox write or transient validation error).
await recordOrganizationInviteAudit(invite.id, orgId, 'EMAIL_SENT', {
actorProfileId: invitedByProfileId,
targetEmail: email