feat: add inquiry notification service for sales team targeting

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-14 12:58:55 -04:00
parent 9a0c28020d
commit c5c45accfc

View File

@@ -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<void> {
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<string[]> {
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<string>();
for (const row of assignments) {
const perms = row.permissions as RolePermissions | null;
if (perms?.interests?.view) {
userIds.add(row.userId);
}
}
return Array.from(userIds);
}