* feat(notifications): add email notification preferences Implement the email notification preferences PRD with org-level defaults and personal opt-outs enforced at the email enqueue chokepoint. - Add toggle map, profile.settings jsonb, and shouldSendEmail resolver - Extend profile and organization update APIs with emailNotifications - Pass preference context through all toggleable email send call sites - Add Settings > Notifications page with personal and org admin sections Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * feat(notifications): group email toggles into course, cohort, and enrollment sections Organize personal and org notification settings into scannable sections so users can find course- or cohort-related emails quickly. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(notifications): use Exercise instead of Quiz in toggle copy Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(notifications): only mark certification email sent when enqueued Skip setMemberCertificationEmailSent when notification preferences or missing Redis prevent the completion email from being queued, so the email can still send if preferences change later. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(notifications): address PR review feedback - Add EmailPreferenceLookupCache to avoid N+1 settings lookups in batch processors and multi-recipient email enqueue paths - Replace $effect store hydration in notifications settings with onMount and explicit sync after save - Remove thin api email-preferences re-export; import from db queries - Hide tutor-only personal toggles (newStudent, newSubmission) for students Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(notifications): hydrate toggles after store load and add profile settings migration - One-time store hydration via onMount plus guarded effect when profile/org load after mount, without clobbering in-progress edits - Add drizzle migration for profile.settings jsonb column Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> ---------
51 lines
1.8 KiB
TypeScript
51 lines
1.8 KiB
TypeScript
import * as z from 'zod';
|
|
|
|
/**
|
|
* Org branding carried through to the `quizAssigned` email. Mirrors
|
|
* `@cio/email`'s `ZEmailBranding`; defined inline so this lean queue package
|
|
* doesn't take a dependency on the email package. Re-validated against the
|
|
* email template schema at send time.
|
|
*/
|
|
const ZNotifyBranding = z
|
|
.object({
|
|
orgName: z.string().optional(),
|
|
logoUrl: z.string().url().optional(),
|
|
themeColor: z
|
|
.string()
|
|
.regex(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/)
|
|
.optional()
|
|
})
|
|
.optional();
|
|
|
|
/**
|
|
* Payload for a `notifications:notify-course-exercise` job. Fired when a
|
|
* teacher clicks "Notify students" on an exercise — the worker fans out one
|
|
* `quizAssigned` email per course member with a deep link to take the quiz.
|
|
*/
|
|
export const ZNotifyCourseExercisePayload = z.object({
|
|
courseId: z.string().min(1),
|
|
exerciseTitle: z.string().min(1),
|
|
courseName: z.string().min(1),
|
|
orgName: z.string().min(1),
|
|
organizationId: z.string().min(1),
|
|
fromName: z.string().min(1),
|
|
quizUrl: z.string().min(1),
|
|
branding: ZNotifyBranding
|
|
});
|
|
export type TNotifyCourseExercisePayload = z.infer<typeof ZNotifyCourseExercisePayload>;
|
|
|
|
/** Periodic scan that emails 24h/1h reminders for upcoming live sessions. No payload. */
|
|
export const ZSessionReminderScanPayload = z.object({}).default({});
|
|
export type TSessionReminderScanPayload = z.infer<typeof ZSessionReminderScanPayload>;
|
|
|
|
/**
|
|
* Payload for a `notifications:notify-course-session-update` job. Fired when a
|
|
* teacher changes a live lesson's time/link — emails every member an updated
|
|
* calendar invite (same UID, higher SEQUENCE).
|
|
*/
|
|
export const ZNotifyCourseSessionUpdatePayload = z.object({
|
|
courseId: z.string().min(1),
|
|
lessonId: z.string().min(1)
|
|
});
|
|
export type TNotifyCourseSessionUpdatePayload = z.infer<typeof ZNotifyCourseSessionUpdatePayload>;
|