From c5c45accfc8da348051a77b9da9f0710fe0d0fde Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 14 Apr 2026 12:58:55 -0400 Subject: [PATCH] feat: add inquiry notification service for sales team targeting Co-Authored-By: Claude Sonnet 4.6 --- .../services/inquiry-notifications.service.ts | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 src/lib/services/inquiry-notifications.service.ts diff --git a/src/lib/services/inquiry-notifications.service.ts b/src/lib/services/inquiry-notifications.service.ts new file mode 100644 index 0000000..ba1e700 --- /dev/null +++ b/src/lib/services/inquiry-notifications.service.ts @@ -0,0 +1,137 @@ +import { eq } from 'drizzle-orm'; + +import { db } from '@/lib/db'; +import { userPortRoles, roles } from '@/lib/db/schema/users'; +import type { RolePermissions } from '@/lib/db/schema/users'; +import { createNotification } from '@/lib/services/notifications.service'; +import { getSetting } from '@/lib/services/settings.service'; +import { getQueue } from '@/lib/queue'; +import { logger } from '@/lib/logger'; + +interface InquiryNotificationParams { + portId: string; + portSlug: string; + interestId: string; + clientFullName: string; + clientEmail: string; + clientPhone: string; + mooringNumber: string | null; + firstName: string; +} + +/** + * Sends inquiry notifications to all relevant parties: + * 1. Confirmation email to the client + * 2. In-app + email notifications to CRM users with interests.view permission + * 3. Email to any external recipients configured in system settings + * + * All operations are fire-and-forget (errors are logged, not thrown). + */ +export async function sendInquiryNotifications(params: InquiryNotificationParams): Promise { + const { + portId, + portSlug, + interestId, + clientFullName, + clientEmail, + clientPhone, + mooringNumber, + firstName, + } = params; + + // 1. Queue client confirmation email + try { + const contactEmailSetting = await getSetting('inquiry_contact_email', portId); + const contactEmail = + typeof contactEmailSetting?.value === 'string' + ? contactEmailSetting.value + : 'sales@portnimara.com'; + + const emailQueue = getQueue('email'); + await emailQueue.add('send-inquiry-confirmation', { + to: clientEmail, + firstName, + mooringNumber, + contactEmail, + }); + } catch (err) { + logger.error({ err, interestId }, 'Failed to queue client confirmation email'); + } + + // 2. Notify CRM users with interests.view permission on this port + try { + const usersWithAccess = await findUsersWithInterestsPermission(portId); + const crmUrl = `/${portSlug}/interests/${interestId}`; + + for (const userId of usersWithAccess) { + try { + await createNotification({ + portId, + userId, + type: 'new_registration', + title: 'New Interest Registered', + description: `${clientFullName} has registered interest${mooringNumber ? ` in Berth ${mooringNumber}` : ''} via the website`, + link: crmUrl, + entityType: 'interest', + entityId: interestId, + dedupeKey: `inquiry-${interestId}`, + }); + } catch (err) { + logger.error({ err, userId, interestId }, 'Failed to create notification for user'); + } + } + } catch (err) { + logger.error({ err, interestId }, 'Failed to notify CRM users'); + } + + // 3. Notify external recipients + try { + const recipientsSetting = await getSetting('inquiry_notification_recipients', portId); + const externalEmails: string[] = Array.isArray(recipientsSetting?.value) + ? recipientsSetting.value + : []; + + if (externalEmails.length > 0) { + const emailQueue = getQueue('email'); + const appUrl = process.env.APP_URL ?? ''; + const crmUrl = `${appUrl}/${portSlug}/interests/${interestId}`; + + for (const externalEmail of externalEmails) { + await emailQueue.add('send-inquiry-sales-notification', { + to: externalEmail, + fullName: clientFullName, + email: clientEmail, + phone: clientPhone, + mooringNumber, + crmUrl, + }); + } + } + } catch (err) { + logger.error({ err, interestId }, 'Failed to notify external recipients'); + } +} + +/** + * Finds all user IDs on a port whose role grants `interests.view` permission. + */ +async function findUsersWithInterestsPermission(portId: string): Promise { + const assignments = await db + .select({ + userId: userPortRoles.userId, + permissions: roles.permissions, + }) + .from(userPortRoles) + .innerJoin(roles, eq(userPortRoles.roleId, roles.id)) + .where(eq(userPortRoles.portId, portId)); + + const userIds = new Set(); + for (const row of assignments) { + const perms = row.permissions as RolePermissions | null; + if (perms?.interests?.view) { + userIds.add(row.userId); + } + } + + return Array.from(userIds); +}