From b8a5182791e8bc5f371e177bbed356e0840d8c03 Mon Sep 17 00:00:00 2001 From: Astha Singh Date: Fri, 22 May 2026 23:00:54 +0530 Subject: [PATCH] feat(help): add mentor assignment email notification --- .env.example | 4 ++ src/inngest/functions/help-dispatch.ts | 29 ++++++++++++- src/lib/email.test.ts | 15 +++++++ src/lib/email.ts | 59 ++++++++++++++++++++++++++ 4 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 src/lib/email.test.ts create mode 100644 src/lib/email.ts diff --git a/.env.example b/.env.example index f4cca5d..1b4b68e 100644 --- a/.env.example +++ b/.env.example @@ -28,6 +28,10 @@ KV_REST_API_READ_ONLY_TOKEN= INNGEST_EVENT_KEY= INNGEST_SIGNING_KEY= +# Email +RESEND_API_KEY= +EMAIL_FROM=onboarding@resend.dev + # Groq (LLM) GROQ_API_KEY= GROQ_BUDGET_PER_DAY=400000 diff --git a/src/inngest/functions/help-dispatch.ts b/src/inngest/functions/help-dispatch.ts index 7c27077..fa4d91c 100644 --- a/src/inngest/functions/help-dispatch.ts +++ b/src/inngest/functions/help-dispatch.ts @@ -1,6 +1,7 @@ import { inngest } from '../client'; import { getServiceSupabase } from '@/lib/supabase/service'; import { rankReviewers, type ReviewerCandidate } from '@/lib/help/dispatch'; +import { sendHelpDispatchEmail } from '@/lib/email'; /** * Help dispatch: when a user fires a help request, fan out notifications to @@ -25,7 +26,7 @@ export const helpDispatch = inngest.createFunction( const { data: mentee } = await sb .from('profiles') - .select('level, primary_language') + .select('level, primary_language, github_handle') .eq('id', userId) .maybeSingle(); const menteeLevel = mentee?.level ?? 0; @@ -42,7 +43,7 @@ export const helpDispatch = inngest.createFunction( // Pool: all L2+ profiles. In production we'd narrow by recent activity etc. const { data: pool } = await sb .from('profiles') - .select('id, level, primary_language') + .select('id, level, primary_language, github_handle, email') .gte('level', 2) .neq('id', userId); @@ -68,6 +69,12 @@ export const helpDispatch = inngest.createFunction( if (targets.length === 0) return { notified: 0 }; + const { data: helpRequest } = await sb + .from('help_requests') + .select('reason, pr_url') + .eq('id', helpRequestId) + .maybeSingle(); + // Write a notification row per target for the help-inbox to pick up. const rows = targets.map((t) => ({ user_id: t.userId, @@ -76,6 +83,24 @@ export const helpDispatch = inngest.createFunction( })); await sb.from('activity_log').insert(rows); + for (const target of targets) { + const mentor = pool?.find((p) => p.id === target.userId); + + if (!mentor?.email) continue; + + try { + await sendHelpDispatchEmail({ + to: mentor.email, + mentorHandle: mentor.github_handle ?? 'mentor', + menteeHandle: mentee?.github_handle ?? 'contributor', + prUrl: helpRequest?.pr_url ?? '', + helpReason: helpRequest?.reason ?? null, + }); + } catch (error) { + console.error('failed to send help dispatch email', error); + } + } + return { notified: targets.length }; }); diff --git a/src/lib/email.test.ts b/src/lib/email.test.ts new file mode 100644 index 0000000..7d5fe20 --- /dev/null +++ b/src/lib/email.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from 'vitest'; +import { sendHelpDispatchEmail } from './email'; + +describe('sendHelpDispatchEmail', () => { + it('skips sending when resend is not configured', async () => { + const result = await sendHelpDispatchEmail({ + to: 'test@example.com', + mentorHandle: 'mentor', + menteeHandle: 'mentee', + prUrl: 'https://github.com/test/pr/1', + }); + + expect(result).toEqual({ skipped: true }); + }); +}); diff --git a/src/lib/email.ts b/src/lib/email.ts new file mode 100644 index 0000000..78ada1e --- /dev/null +++ b/src/lib/email.ts @@ -0,0 +1,59 @@ +import { Resend } from 'resend'; + +type SendHelpDispatchEmailArgs = { + to: string; + mentorHandle: string; + menteeHandle: string; + prUrl: string; + helpReason?: string | null; +}; + +const resendApiKey = process.env.RESEND_API_KEY; + +const resend = resendApiKey ? new Resend(resendApiKey) : null; + +export async function sendHelpDispatchEmail({ + to, + mentorHandle, + menteeHandle, + prUrl, + helpReason, +}: SendHelpDispatchEmailArgs) { + if (!resend) { + console.warn('RESEND_API_KEY missing, skipping email send'); + return { skipped: true }; + } + + return resend.emails.send({ + from: process.env.EMAIL_FROM || 'onboarding@resend.dev', + to, + subject: '[MergeShip] Someone needs your help on a PR', + html: ` +

Someone needs your help on a PR

+ +

Hello ${mentorHandle},

+ +

${menteeHandle} has requested help on a pull request.

+ +

+ Pull Request:
+ ${prUrl} +

+ + ${ + helpReason + ? ` +

+ Help Request:
+ ${helpReason} +

+ ` + : '' + } + +

+ Visit the Help Inbox to respond and assist the contributor. +

+ `, + }); +}