From df7a4617e008174624c02bc89905af389f7c478d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 13:22:39 -0400 Subject: [PATCH 1/2] fix(app): send emails to employees when all policies are published (#1707) Co-authored-by: chasprowebdev --- apps/app/src/actions/policies/publish-all.ts | 48 ++++++- .../tasks/email/publish-all-policies-email.ts | 48 +++++++ .../email/emails/all-policy-notification.tsx | 118 ++++++++++++++++++ packages/email/index.ts | 2 + packages/email/lib/all-policy-notification.ts | 42 +++++++ 5 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 apps/app/src/jobs/tasks/email/publish-all-policies-email.ts create mode 100644 packages/email/emails/all-policy-notification.tsx create mode 100644 packages/email/lib/all-policy-notification.ts diff --git a/apps/app/src/actions/policies/publish-all.ts b/apps/app/src/actions/policies/publish-all.ts index c164efc79..4aa24a9e5 100644 --- a/apps/app/src/actions/policies/publish-all.ts +++ b/apps/app/src/actions/policies/publish-all.ts @@ -1,6 +1,7 @@ 'use server'; -import { db, PolicyStatus } from '@db'; +import { sendPublishAllPoliciesEmail } from '@/jobs/tasks/email/publish-all-policies-email'; +import { db, PolicyStatus, Role } from '@db'; import { revalidatePath } from 'next/cache'; import { z } from 'zod'; import { authActionClient } from '../safe-action'; @@ -93,6 +94,51 @@ export const publishAllPoliciesAction = authActionClient } } + // Get organization info and all members to send emails + const organization = await db.organization.findUnique({ + where: { id: parsedInput.organizationId }, + select: { name: true }, + }); + + const members = await db.member.findMany({ + where: { + organizationId: parsedInput.organizationId, + isActive: true, + role: { + contains: Role.employee, + }, + }, + include: { + user: { + select: { + email: true, + name: true, + }, + }, + }, + }); + + // Trigger email tasks for all employees using batchTrigger + const emailPayloads = members + .filter((orgMember) => orgMember.user.email) + .map((orgMember) => ({ + payload: { + email: orgMember.user.email, + userName: orgMember.user.name || 'there', + organizationName: organization?.name || 'Your organization', + organizationId: parsedInput.organizationId, + }, + })); + + if (emailPayloads.length > 0) { + try { + await sendPublishAllPoliciesEmail.batchTrigger(emailPayloads); + } catch (emailError) { + console.error('[publish-all-policies] Failed to trigger bulk emails:', emailError); + // Don't throw - the policies are published successfully + } + } + revalidatePath(`/${parsedInput.organizationId}/policies`); revalidatePath(`/${parsedInput.organizationId}/frameworks`); return { diff --git a/apps/app/src/jobs/tasks/email/publish-all-policies-email.ts b/apps/app/src/jobs/tasks/email/publish-all-policies-email.ts new file mode 100644 index 000000000..ae03d786c --- /dev/null +++ b/apps/app/src/jobs/tasks/email/publish-all-policies-email.ts @@ -0,0 +1,48 @@ +import { sendAllPolicyNotificationEmail } from '@comp/email'; +import { logger, queue, task } from '@trigger.dev/sdk'; + +// Queue with concurrency limit to ensure rate limiting +const allPolicyEmailQueue = queue({ + name: 'all-policy-email-queue', + concurrencyLimit: 2, +}); + +interface AllPolicyEmailPayload { + email: string; + userName: string; + organizationId: string; + organizationName: string; +} + +export const sendPublishAllPoliciesEmail = task({ + id: 'send-publish-all-policies-email', + queue: allPolicyEmailQueue, + run: async (payload: AllPolicyEmailPayload) => { + logger.info('Sending all policies published email', { + email: payload.email, + organizationName: payload.organizationName, + }); + + try { + await sendAllPolicyNotificationEmail(payload); + + logger.info('Successfully sent all policies email', { + email: payload.email, + organizationName: payload.organizationName, + }); + + return { + success: true, + email: payload.email, + }; + } catch (error) { + logger.error('Failed to send all policies email', { + email: payload.email, + error: error instanceof Error ? error.message : String(error), + }); + + throw error; + } + }, +}); + diff --git a/packages/email/emails/all-policy-notification.tsx b/packages/email/emails/all-policy-notification.tsx new file mode 100644 index 000000000..89275b835 --- /dev/null +++ b/packages/email/emails/all-policy-notification.tsx @@ -0,0 +1,118 @@ +import { + Body, + Button, + Container, + Font, + Heading, + Html, + Link, + Preview, + Section, + Tailwind, + Text, +} from '@react-email/components'; +import { Footer } from '../components/footer'; +import { Logo } from '../components/logo'; + +interface Props { + email: string; + userName: string; + organizationName: string; + organizationId: string; +} + +export const AllPolicyNotificationEmail = ({ + email, + userName, + organizationName, + organizationId, +}: Props) => { + const link = `${process.env.NEXT_PUBLIC_PORTAL_URL ?? 'https://portal.trycomp.ai'}/${organizationId}`; + const subjectText = 'Please review and accept the policies'; + + return ( + + + + + + + + + {subjectText} + + + + + + {subjectText} + + + + Hi {userName}, + + + + All policies have been published and require your review. + + + + Your organization {organizationName} requires all employees to review and accept these policies. + + +
+ +
+ + + or copy and paste this URL into your browser{' '} + + {link} + + + +
+
+ + This notification was intended for {email}. + +
+ +
+ +