feat: add inquiry notification service for sales team targeting
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
137
src/lib/services/inquiry-notifications.service.ts
Normal file
137
src/lib/services/inquiry-notifications.service.ts
Normal 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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user