From 8288f157b36ec5094d6968771e5c73b4eafab3ad Mon Sep 17 00:00:00 2001 From: Rotimi Best Date: Sun, 12 Jul 2026 18:04:07 +0100 Subject: [PATCH] fix(email): include course name in student welcome subject (#739) * fix(email): include course name in student welcome subject The studentCourseWelcome template used a static subject, so bulk course assignments produced identical inbox titles. Resolve the subject from courseName at send time and sanitize it like other email headers. Co-authored-by: Rotimi Best * fix(email): use course-only subject for student welcome email Co-authored-by: Rotimi Best --------- Co-authored-by: Cursor Agent Co-authored-by: Rotimi Best --- packages/email/src/core/base.ts | 7 ++++++- packages/email/src/core/types.ts | 4 ++-- .../email/src/emails/student-course-welcome.ts | 2 +- .../email/tests/student-course-welcome.test.ts | 14 ++++++++++++++ 4 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 packages/email/tests/student-course-welcome.test.ts diff --git a/packages/email/src/core/base.ts b/packages/email/src/core/base.ts index f0e24e2e..54dd4070 100644 --- a/packages/email/src/core/base.ts +++ b/packages/email/src/core/base.ts @@ -3,6 +3,7 @@ import * as z from 'zod'; import type { EmailResponse } from '../utils/types'; import type { SendTemplateConfig } from './types'; import { deliverEmail } from '../send'; +import { sanitizeEmailSubject } from '../utils/functions/email-helpers'; /** * Send a single email using a template @@ -13,10 +14,14 @@ export async function sendTemplateEmail( ): Promise { const content = config.template.render(config.fields); + const resolvedSubject = + config.subject ?? + (typeof config.template.subject === 'function' ? config.template.subject(config.fields) : config.template.subject); + return deliverEmail([ { to: config.to, - subject: config.subject ?? config.template.subject, + subject: sanitizeEmailSubject(resolvedSubject), content, from: config.from ?? config.template.from, replyTo: config.replyTo ?? config.template.replyTo, diff --git a/packages/email/src/core/types.ts b/packages/email/src/core/types.ts index e4442ecc..221ae642 100644 --- a/packages/email/src/core/types.ts +++ b/packages/email/src/core/types.ts @@ -6,7 +6,7 @@ import type { EmailId } from '../utils/types'; * Base email template interface */ export interface EmailTemplate { - subject: string; + subject: string | ((fields: z.infer) => string); schema: TSchema; render: (fields: z.infer) => string; from?: string; @@ -47,7 +47,7 @@ export interface SendTemplateConfig exten */ export interface DefineEmailConfig { id: EmailId; - subject: string; + subject: string | ((fields: z.infer) => string); schema: TSchema; render: (fields: z.infer) => string; from?: string; diff --git a/packages/email/src/emails/student-course-welcome.ts b/packages/email/src/emails/student-course-welcome.ts index fd25fb65..21d02107 100644 --- a/packages/email/src/emails/student-course-welcome.ts +++ b/packages/email/src/emails/student-course-welcome.ts @@ -6,7 +6,7 @@ import { ZEmailBranding } from '../core/branding'; export const studentCourseWelcomeEmail = defineEmail({ id: 'studentCourseWelcome', - subject: 'You have access to a course — sign in to get started', + subject: (fields) => `You have access to ${fields.courseName} course`, schema: z.object({ orgName: z.string().min(1), courseName: z.string().min(1), diff --git a/packages/email/tests/student-course-welcome.test.ts b/packages/email/tests/student-course-welcome.test.ts new file mode 100644 index 00000000..9a8df4d5 --- /dev/null +++ b/packages/email/tests/student-course-welcome.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'vitest'; + +import { studentCourseWelcomeEmail } from '../src/emails/student-course-welcome'; + +describe('studentCourseWelcomeEmail', () => { + it('includes the course name in the subject', () => { + const resolveSubject = studentCourseWelcomeEmail.template.subject; + + expect(typeof resolveSubject).toBe('function'); + expect((resolveSubject as (fields: { courseName: string }) => string)({ courseName: 'React Basics' })).toBe( + 'You have access to React Basics course' + ); + }); +});