-
Notifications
You must be signed in to change notification settings - Fork 264
Phase 1: Migrate to Email Notification System (with unit tests) #1082
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
jobs/generateDailyReports.ts
Outdated
| export async function generateMailboxDailyEmailReport() { | ||
| const mailbox = await db.query.mailboxes.findFirst({ | ||
| where: isNull(sql`${mailboxes.preferences}->>'disabled'`), | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inlined getMailbox() because it was imported from server-only file which would cause error while using react-email/components
| }) | ||
| .from(userProfiles) | ||
| .innerJoin(authUsers, eq(userProfiles.id, authUsers.id)) | ||
| .where(or(isNull(userProfiles.preferences), sql`${userProfiles.preferences}->>'allowDailyEmail' != 'false'`)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
where(or(isNull(userProfiles.preferences), sql`${userProfiles.preferences}->>'allowDailyEmail' != 'false'`))This condition checks for user preferences and doesn't sends email only if user have manually changed preference via UI.
| export async function generateWeeklyEmailReports() { | ||
| const mailbox = await db.query.mailboxes.findFirst({ | ||
| where: isNull(sql`${mailboxes.preferences}->>'disabled'`), | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again inlined getMailbox() due to same reason.
| }) | ||
| .from(userProfiles) | ||
| .innerJoin(authUsers, eq(userProfiles.id, authUsers.id)) | ||
| .where(or(isNull(userProfiles.preferences), sql`${userProfiles.preferences}->>'allowWeeklyEmail' != 'false'`)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Checking for user preference before sending email.
| await db.update(conversationMessages).set({ cleanedUpText: cleaned }).where(eq(conversationMessages.id, m.id)); | ||
| return cleaned; | ||
| }; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inlined ensureCleanedUpText , generateCleanedUpText , determineVipStatus as they are imported from server-only file.
| const mailbox = assertDefinedOrRaiseNonRetriableError(await getMailbox()); | ||
| const mailbox = await db.query.mailboxes.findFirst({ | ||
| where: isNull(sql`${mailboxes.preferences}->>'disabled'`), | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inlined getMailbox()
| const conversationLink = `${getBaseUrl()}/conversations?id=${conversation.slug}`; | ||
| const customerLinks = platformCustomerRecord?.links | ||
| ? Object.entries(platformCustomerRecord.links).map(([key, value]) => ({ label: key, url: value })) | ||
| : undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| style={{ color: "#6b7280", textDecoration: "none" }} | ||
| > | ||
| <Img | ||
| //src={`${baseUrl}/logo_mahogany_900_for_email.png`} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sending an email using this as the src for the <img> tag doesn’t seem to work during local testing, but this is how it’s sent in other emails
| /> | ||
| <SummaryRow label="Average reply time" value={avgReplyTime ?? "—"} /> | ||
| <SummaryRow label="VIP average reply time" value={vipAvgReplyTime ?? "—"} /> | ||
| <SummaryRow label="Average time existing open tickets have been open" value={avgWaitTime ?? "—"} last /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- is shown in above 3 cases, only when no mailbox message haven been replied
| new Error(`Daily report: failed to send ${failures.length}/${emailResults.length} daily emails`), | ||
| { extra: { failures } }, | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added better logging and sentry capturing when email sending fails. Usually happens when email is malformed, dns/tls error, resend rate limiting etc..
| captureExceptionAndLog(error); | ||
| throw error; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Split the code into two function so that it is standardized and consistent with generateWeeklyReport.tsx . This also makes it easier for the importing function to run unit tests without explicitly setting the environment variables.
| "i", | ||
| ); | ||
| expect(rx.test(html)).toBe(true); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because the daily email is sent as table element with each value inside <td> this makes it difficult for checking by this usual method
expect.toContain("Open tickets", 2);|
@slavingia Just wanted to clarify, the description mentions migrations for Should I migrate those as well? If yes, would you prefer that I include them in this PR or create a separate one instead? (Considering the diffs in the current PR, creating a separate one might make it easier to review.) |
| ]; | ||
| export async function generateMailboxDailyEmailReport(): Promise<MailboxDailyEmailReportResult> { | ||
| try { | ||
| const mailbox = await db.query.mailboxes.findFirst({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inlined getMailbox() because it was imported from server-only file which would cause error while using react-email/components

This PR implements Phase 1 of #1075
Summary
allowDailyEmail,allowWeeklyEmail,allowVipMessageEmail) to frontend and added E2E and unit tests for same.enabled— emails are sent unless explicitly set tofalse.jobs/notifyVipMessageEmaillib/resend/client.tsinorder to follow DRY principle.Changes
Jobs
jobs/generateDailyReports.ts,jobs/generateWeeklyReports.ts,jobs/notifyVipMessage.tsUser Preferences
Updated
userProfiles.preferences(already JSON-capable) and TRPC schema to include:Query pattern for recipients:
New UI Components (switches):
Integrated these within
preferencesSetting.tsx.Tests
tests/jobs/generateDailyReport.test.tsandtests/jobs/generateWeeklyReport.test.tstests/jobs/notfiyVipMessageEmail.test.tsEmail Templates
DailyEmailReportTemplateWeeklyEmailReportTemplateVipNotificationEmailTemplateSlack vs Gmail Comparison
Daily Report
Weekly Report
VIP Message Notification
VIP Notification: when VIP replies VS when staff replies Vs when staff replies and closes
UI Changes :
Tests Passing
jobs/generateDailyReport.test.ts
jobs/generateWeeklyReport.test.ts
jobs/notifyVipNotification.test.ts
Proofs :
Screen.Recording.2025-11-12.at.4.05.59.PM.mov
Screen.Recording.2025-11-12.at.5.17.25.PM.mov
Notes
Some helper functions used in the migrated cron jobs had a
"server-only"directive at the top of their files.This caused import issues when used with React Email templates, since those templates require
reactandreact-dom, which cannot be imported into"server-only"contexts.Resulting error:
Error: react-dom/server is not supported in React Server ComponentsTo resolve this, helper functions were inlined directly into the migrated job files. The inlined helper functions are 100% similar to that of original ones.
Affected helpers:
AI Usage
Live Stream Disclosure