Skip to content

Conversation

@stefanbinoj
Copy link
Contributor

@stefanbinoj stefanbinoj commented Nov 8, 2025

This PR implements Phase 1 of #1075

Summary

  • Migrated cron jobs and trigger.
  • Added user preferences (allowDailyEmail, allowWeeklyEmail, allowVipMessageEmail) to frontend and added E2E and unit tests for same.
    • Preferences default to enabled — emails are sent unless explicitly set to false.
  • Added new UI components and integrated them into the settings page.
  • Updated concerned unit tests.
  • Wrote new unit test for jobs/notifyVipMessageEmail
  • Created a resend client lib/resend/client.ts inorder to follow DRY principle.
  • Manually tested by temporarily shortening scheduler intervals and verifying sent email results.

Changes

Jobs

  • Migrated jobs/generateDailyReports.ts , jobs/generateWeeklyReports.ts , jobs/notifyVipMessage.ts

User Preferences
Updated userProfiles.preferences (already JSON-capable) and TRPC schema to include:

allowDailyEmail?: boolean; // default enabled when undefined
allowWeeklyEmail?: boolean; // default enabled when undefined
allowVipMessageEmail?: boolean; // default enabled when undefined

Query pattern for recipients:

... WHERE preferences IS NULL OR preferences->>'allowDailyEmail' != 'false'

New UI Components (switches):

dailyEmailSetting.tsx
weeklyEmailSetting.tsx
vipMessageEmailSetting.tsx

Integrated these within preferencesSetting.tsx.

Tests

  • Updated tests/jobs/generateDailyReport.test.ts and tests/jobs/generateWeeklyReport.test.ts
  • Added test tests/jobs/notfiyVipMessageEmail.test.ts
  • For each of unit tests added new test for checking emails based on user preferences.
  • Added E2E tests for testing user preferences.

Email Templates

  • Daily: DailyEmailReportTemplate
  • Weekly: WeeklyEmailReportTemplate
  • VIP: VipNotificationEmailTemplate

Slack vs Gmail Comparison

Daily Report

Slack Gmail
Daily Report - Slack Screenshot 2025-11-12 at 3 55 54 PM

Weekly Report

Slack Gmail
Screenshot 2025-11-10 at 2 19 54 PM Screenshot 2025-11-10 at 10 15 31 AM

VIP Message Notification

Slack Gmail
VIP Message - Slack Screenshot 2025-11-10 at 10 42 01 AM

VIP Notification: when VIP replies VS when staff replies Vs when staff replies and closes

When VIP Replies When Staff Replies When Staff Replies And Closes
Screenshot 2025-11-10 at 10 42 01 AM Screenshot 2025-11-12 at 4 30 31 PM Screenshot 2025-11-12 at 4 01 43 PM

UI Changes :

Desktop Mobile
Screenshot 2025-11-10 at 11 57 24 AM Screenshot 2025-11-10 at 11 57 56 AM

Tests Passing

E2E Unit Tests
Screenshot 2025-11-13 at 12 57 06 PM

jobs/generateDailyReport.test.ts

Screenshot 2025-11-15 at 7 32 49 PM

jobs/generateWeeklyReport.test.ts

Screenshot 2025-11-13 at 1 45 07 PM

jobs/notifyVipNotification.test.ts

Screenshot 2025-11-13 at 1 45 26 PM

Proofs :

  1. Email triggered when staff replies to vip
Screen.Recording.2025-11-12.at.4.05.59.PM.mov
  1. Cron job running (Updated cron jobs to run every alternative minute for sake of demonstration)
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 react and react-dom, which cannot be imported into "server-only" contexts.

Resulting error:

Error: react-dom/server is not supported in React Server Components

To 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:

1. getMailBox()
2. determineVipStatus()
3. ensureCleanedUpText()

AI Usage

  • GPT-5 (Medium, via VS Code): Assisted in drafting email template structures, UI component scaffolds, and initial E2E and unit test drafts.
  • Clause 4.5 (Haiky, via VS Code): Helped with environment setup ngrok, Understanding Postgres job queues, Vercel function behavior.
  • GPT (Free, via chat.openai.com): Used for formatting this PR description.
  • Devin's PR : this PR was helpful for understanding the changes and desired outcome.

    All AI-assisted code was manually reviewed and tested.

Live Stream Disclosure

  • I have watched all Antiwork live streams and followed the provided guidelines accordingly.

@stefanbinoj stefanbinoj changed the title Phase 1: Build and Integrate the Email Notification System [WIP] Phase 1: Build and Integrate the Email Notification System Nov 9, 2025
@stefanbinoj stefanbinoj marked this pull request as draft November 9, 2025 06:12
export async function generateMailboxDailyEmailReport() {
const mailbox = await db.query.mailboxes.findFirst({
where: isNull(sql`${mailboxes.preferences}->>'disabled'`),
});
Copy link
Contributor Author

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'`));
Copy link
Contributor Author

@stefanbinoj stefanbinoj Nov 10, 2025

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'`),
});
Copy link
Contributor Author

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'`));
Copy link
Contributor Author

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;
};

Copy link
Contributor Author

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'`),
});
Copy link
Contributor Author

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;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

conversationLink and customerLinks are used for redirection and impersonation
Screenshot 2025-11-10 at 10 42 01 AM copy

style={{ color: "#6b7280", textDecoration: "none" }}
>
<Img
//src={`${baseUrl}/logo_mahogany_900_for_email.png`}
Copy link
Contributor Author

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 />
Copy link
Contributor Author

@stefanbinoj stefanbinoj Nov 12, 2025

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 } },
);
}
Copy link
Contributor Author

@stefanbinoj stefanbinoj Nov 13, 2025

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;
}
}
Copy link
Contributor Author

@stefanbinoj stefanbinoj Nov 13, 2025

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);
};
Copy link
Contributor Author

@stefanbinoj stefanbinoj Nov 13, 2025

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);

@stefanbinoj stefanbinoj changed the title [WIP] Phase 1: Build and Integrate the Email Notification System Phase 1: Integrate Email Notification System (with unit tests) Nov 13, 2025
@stefanbinoj stefanbinoj changed the title Phase 1: Integrate Email Notification System (with unit tests) Phase 1: Migrate to Email Notification System (with unit tests) Nov 13, 2025
@stefanbinoj stefanbinoj marked this pull request as ready for review November 13, 2025 08:56
@stefanbinoj
Copy link
Contributor Author

@slavingia Just wanted to clarify, the description mentions migrations for Ticket response time alerts and Knowledge bank suggestion notifications, but Devin’s PR doesn’t seem to include any relevant changes for those.

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({
Copy link
Contributor Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant