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 <[email protected]>

* fix(email): use course-only subject for student welcome email

Co-authored-by: Rotimi Best <[email protected]>

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
Rotimi Best
2026-07-12 18:04:07 +01:00
committed by GitHub
co-authored by Rotimi Best Cursor Agent
parent 51f90bf450
commit 8288f157b3
4 changed files with 23 additions and 4 deletions
+6 -1
View File
@@ -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<TSchema extends z.ZodType>(
): Promise<EmailResponse[]> {
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,
+2 -2
View File
@@ -6,7 +6,7 @@ import type { EmailId } from '../utils/types';
* Base email template interface
*/
export interface EmailTemplate<TSchema extends z.ZodType = z.ZodType> {
subject: string;
subject: string | ((fields: z.infer<TSchema>) => string);
schema: TSchema;
render: (fields: z.infer<TSchema>) => string;
from?: string;
@@ -47,7 +47,7 @@ export interface SendTemplateConfig<TSchema extends z.ZodType = z.ZodType> exten
*/
export interface DefineEmailConfig<TSchema extends z.ZodType = z.ZodType> {
id: EmailId;
subject: string;
subject: string | ((fields: z.infer<TSchema>) => string);
schema: TSchema;
render: (fields: z.infer<TSchema>) => string;
from?: string;
@@ -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),
@@ -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'
);
});
});