Skip to content

Commit 5a0e5b4

Browse files
Merge pull request #1999 from OneCommunityGlobal/messaging-backend
uha FIX LB backend messaging
2 parents 25ce3d1 + 186c3ae commit 5a0e5b4

6 files changed

Lines changed: 235 additions & 45 deletions

File tree

jest.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ module.exports = {
4242
transform: {
4343
'^.+\\.js$': 'babel-jest',
4444
},
45+
moduleNameMapper: {
46+
'^puppeteer$': '<rootDir>/src/test/mocks/puppeteer.js',
47+
},
4548
setupFilesAfterEnv: ['<rootDir>/src/test/setup.js'],
4649
// Simple CI settings
4750
maxWorkers: 1, // Run tests sequentially

src/controllers/lbdashboard/lbuserPrefController.js

Lines changed: 116 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,72 @@
1+
const mongoose = require('mongoose');
2+
13
const lbUserPrefController = function (UserPreferences, Notification) {
4+
const normalizeObjectId = (value) => {
5+
if (typeof value !== 'string') return null;
6+
7+
const trimmed = value.trim();
8+
if (!mongoose.Types.ObjectId.isValid(trimmed)) return null;
9+
10+
return trimmed;
11+
};
12+
13+
const normalizeObjectIdList = (values) => {
14+
if (!Array.isArray(values)) return null;
15+
16+
const normalizedIds = values.map(normalizeObjectId);
17+
return normalizedIds.every(Boolean) ? normalizedIds : null;
18+
};
19+
20+
const normalizePhone = (phone) => {
21+
if (!phone) return { normalized: '', last4: '' };
22+
const trimmed = String(phone).trim();
23+
const hasPlus = trimmed.startsWith('+');
24+
const digits = trimmed.replace(/\D/g, '');
25+
const normalized = hasPlus ? `+${digits}` : digits;
26+
return { normalized, last4: digits.slice(-4) };
27+
};
28+
29+
const maskPhone = (phone) => {
30+
if (!phone) return '';
31+
const digits = String(phone).replace(/\D/g, '');
32+
if (digits.length <= 4) return digits;
33+
return `***-***-${digits.slice(-4)}`;
34+
};
235
const getPreferences = async (req, res) => {
336
try {
437
const { userId, selectedUserId } = req.body;
38+
const normalizedUserId = normalizeObjectId(userId);
39+
const normalizedSelectedUserId = selectedUserId
40+
? normalizeObjectId(selectedUserId)
41+
: null;
542

6-
if (!userId) {
7-
return res.status(400).json({ message: 'User ID is required.' });
43+
if (!normalizedUserId) {
44+
return res.status(400).json({ message: 'A valid user ID is required.' });
845
}
946

10-
const preferences = await UserPreferences.findOne({ user: userId }).populate(
47+
if (selectedUserId && !normalizedSelectedUserId) {
48+
return res.status(400).json({ message: 'Selected user ID must be a valid ID.' });
49+
}
50+
51+
const preferences = await UserPreferences.findOne({ user: normalizedUserId }).populate(
1152
'users.userNotifyingFor',
1253
);
1354

1455
if (!preferences) {
1556
return res.status(404).json({ message: 'Preferences not found for the user.' });
1657
}
1758

18-
if (selectedUserId) {
59+
if (normalizedSelectedUserId) {
1960
const selectedUserPref = preferences.users.find(
20-
(pref) => pref.userNotifyingFor._id.toString() === selectedUserId,
61+
(pref) => pref.userNotifyingFor._id.toString() === normalizedSelectedUserId,
2162
);
2263

2364
return res.status(200).json(selectedUserPref || { notifyInApp: false, notifyEmail: false });
2465
}
2566

26-
res.status(200).json(preferences);
67+
const response = preferences.toObject();
68+
response.smsPhoneMasked = maskPhone(preferences.smsPhone);
69+
res.status(200).json(response);
2770
} catch (error) {
2871
console.error('Error fetching preferences:', error);
2972
res.status(500).json({ message: 'Error fetching preferences', error: error.message });
@@ -32,43 +75,69 @@ const lbUserPrefController = function (UserPreferences, Notification) {
3275

3376
const updatePreferences = async (req, res) => {
3477
try {
35-
const { userId, selectedUserId, notifyInApp, notifyEmail } = req.body;
78+
const { userId, selectedUserId, notifyInApp, notifyEmail, notifySms, smsPhone } = req.body;
79+
const normalizedUserId = normalizeObjectId(userId);
80+
const normalizedSelectedUserId = selectedUserId
81+
? normalizeObjectId(selectedUserId)
82+
: null;
83+
84+
if (!normalizedUserId) {
85+
return res.status(400).json({ message: 'A valid user ID is required.' });
86+
}
3687

37-
if (!userId || !selectedUserId) {
38-
return res.status(400).json({ message: 'User ID and Selected User ID are required.' });
88+
if (selectedUserId && !normalizedSelectedUserId) {
89+
return res.status(400).json({ message: 'Selected user ID must be a valid ID.' });
3990
}
4091

41-
const preferences = await UserPreferences.findOne({ user: userId });
92+
let preferences = await UserPreferences.findOne({ user: normalizedUserId });
4293

4394
if (!preferences) {
44-
const newPreferences = new UserPreferences({
45-
user: userId,
46-
users: [
47-
{
48-
userNotifyingFor: selectedUserId,
49-
notifyInApp: notifyInApp !== undefined ? notifyInApp : false,
50-
notifyEmail: notifyEmail !== undefined ? notifyEmail : false,
51-
},
52-
],
53-
});
54-
55-
await newPreferences.save();
56-
return res.status(200).json(newPreferences);
95+
preferences = new UserPreferences({ user: normalizedUserId, users: [] });
5796
}
5897

59-
const userIndex = preferences.users.findIndex(
60-
(user) => user.userNotifyingFor.toString() === selectedUserId,
61-
);
98+
if (normalizedSelectedUserId) {
99+
const userIndex = preferences.users.findIndex(
100+
(user) => user.userNotifyingFor.toString() === normalizedSelectedUserId,
101+
);
102+
103+
if (userIndex === -1) {
104+
preferences.users.push({
105+
userNotifyingFor: normalizedSelectedUserId,
106+
notifyInApp: notifyInApp !== undefined ? notifyInApp : false,
107+
notifyEmail: notifyEmail !== undefined ? notifyEmail : false,
108+
});
109+
} else {
110+
preferences.users[userIndex].notifyInApp =
111+
notifyInApp !== undefined ? notifyInApp : false;
112+
preferences.users[userIndex].notifyEmail =
113+
notifyEmail !== undefined ? notifyEmail : false;
114+
}
115+
} else if (notifyInApp !== undefined || notifyEmail !== undefined) {
116+
if (notifyInApp !== undefined) {
117+
preferences.notifyInApp = notifyInApp;
118+
}
119+
if (notifyEmail !== undefined) {
120+
preferences.notifyEmail = notifyEmail;
121+
}
122+
}
62123

63-
if (userIndex === -1) {
64-
preferences.users.push({
65-
userNotifyingFor: selectedUserId,
66-
notifyInApp: notifyInApp !== undefined ? notifyInApp : false,
67-
notifyEmail: notifyEmail !== undefined ? notifyEmail : false,
68-
});
69-
} else {
70-
preferences.users[userIndex].notifyInApp = notifyInApp !== undefined ? notifyInApp : false;
71-
preferences.users[userIndex].notifyEmail = notifyEmail !== undefined ? notifyEmail : false;
124+
if (notifySms !== undefined || smsPhone !== undefined) {
125+
const { normalized, last4 } = normalizePhone(smsPhone);
126+
const digits = normalized.replace(/\D/g, '');
127+
const existingDigits = String(preferences.smsPhone || '').replace(/\D/g, '');
128+
if (notifySms && digits.length === 0 && existingDigits.length === 0) {
129+
return res.status(400).json({ message: 'SMS phone number is required.' });
130+
}
131+
if (digits.length > 0 && (digits.length < 8 || digits.length > 15)) {
132+
return res.status(400).json({ message: 'Invalid phone number format.' });
133+
}
134+
if (notifySms !== undefined) {
135+
preferences.notifySms = notifySms;
136+
}
137+
if (smsPhone !== undefined && digits.length > 0) {
138+
preferences.smsPhone = normalized;
139+
preferences.smsPhoneLast4 = last4;
140+
}
72141
}
73142

74143
const updatedPreferences = await preferences.save();
@@ -82,15 +151,17 @@ const lbUserPrefController = function (UserPreferences, Notification) {
82151
const storeNotification = async (req, res) => {
83152
try {
84153
const { userId, senderId, message } = req.body;
154+
const normalizedUserId = normalizeObjectId(userId);
155+
const normalizedSenderId = normalizeObjectId(senderId);
85156

86-
if (!userId || !senderId || !message) {
157+
if (!normalizedUserId || !normalizedSenderId || !message) {
87158
return res.status(400).json({ message: 'User ID, Sender ID, and Message are required.' });
88159
}
89160

90161
const notification = new Notification({
91162
message,
92-
sender: senderId,
93-
recipient: userId,
163+
sender: normalizedSenderId,
164+
recipient: normalizedUserId,
94165
isSystemGenerated: false,
95166
});
96167

@@ -105,13 +176,14 @@ const lbUserPrefController = function (UserPreferences, Notification) {
105176
const getUnreadNotifications = async (req, res) => {
106177
try {
107178
const { userId } = req.params;
179+
const normalizedUserId = normalizeObjectId(userId);
108180

109-
if (!userId) {
181+
if (!normalizedUserId) {
110182
console.error('❌ User ID is missing in the request.');
111-
return res.status(400).json({ message: 'User ID is required.' });
183+
return res.status(400).json({ message: 'A valid user ID is required.' });
112184
}
113185

114-
const notifications = await Notification.find({ recipient: userId, isRead: false })
186+
const notifications = await Notification.find({ recipient: normalizedUserId, isRead: false })
115187
.sort({ createdTimeStamps: -1 })
116188
.populate('sender', 'firstName lastName'); // Include sender's name
117189

@@ -127,12 +199,13 @@ const lbUserPrefController = function (UserPreferences, Notification) {
127199
const markNotificationsAsRead = async (req, res) => {
128200
try {
129201
const { notificationIds } = req.body;
202+
const normalizedNotificationIds = normalizeObjectIdList(notificationIds);
130203

131-
if (!notificationIds || !Array.isArray(notificationIds)) {
204+
if (!normalizedNotificationIds) {
132205
return res.status(400).json({ message: 'Invalid notification IDs.' });
133206
}
134207
const result = await Notification.updateMany(
135-
{ _id: { $in: notificationIds } },
208+
{ _id: { $in: normalizedNotificationIds } },
136209
{ isRead: true },
137210
);
138211

src/models/lbdashboard/userPreferences.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ const mongoose = require('mongoose');
22

33
const userPreferencesSchema = new mongoose.Schema({
44
user: { type: mongoose.Schema.Types.ObjectId, ref: 'userProfile', required: true },
5+
notifyInApp: { type: Boolean, default: false },
6+
notifyEmail: { type: Boolean, default: false },
7+
notifySms: { type: Boolean, default: false },
8+
smsPhone: { type: String, default: '' },
9+
smsPhoneLast4: { type: String, default: '' },
510
users: [
611
{
712
userNotifyingFor: {

src/test/mocks/puppeteer.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = {};

src/utilities/smsQueue.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
const { twilioSendSMS, TextbeltSMS, TelesignSMS } = require('./SMSSender');
2+
3+
const queue = [];
4+
let processing = false;
5+
6+
const getProvider = () => (process.env.SMS_PROVIDER || 'telesign').toLowerCase();
7+
8+
const sendSmsViaProvider = async (message, to) => {
9+
const provider = getProvider();
10+
if (!to) {
11+
throw new Error('SMS recipient is required.');
12+
}
13+
14+
switch (provider) {
15+
case 'twilio': {
16+
const from = process.env.TWILIO_FROM_NUMBER;
17+
if (!from) {
18+
throw new Error('TWILIO_FROM_NUMBER is not configured.');
19+
}
20+
return twilioSendSMS(message, from, to);
21+
}
22+
case 'textbelt':
23+
return TextbeltSMS(message, to);
24+
case 'telesign':
25+
default:
26+
return TelesignSMS(message, to);
27+
}
28+
};
29+
30+
const processQueue = async () => {
31+
if (processing || queue.length === 0) return;
32+
processing = true;
33+
34+
while (queue.length > 0) {
35+
const item = queue.shift();
36+
if (!item) continue;
37+
try {
38+
const response = await Promise.resolve(sendSmsViaProvider(item.message, item.to));
39+
if (process.env.SMS_LOG_RESPONSES === 'true') {
40+
console.log('SMS send response:', response?.data || response);
41+
}
42+
} catch (error) {
43+
console.error('Failed to send SMS:', error?.response?.data || error.message);
44+
}
45+
}
46+
47+
processing = false;
48+
};
49+
50+
const queueSmsNotification = ({ to, message }) => {
51+
if (!to || !message) return;
52+
queue.push({ to, message });
53+
setImmediate(() => {
54+
processQueue().catch((error) => console.error('SMS queue error:', error.message));
55+
});
56+
};
57+
58+
module.exports = { queueSmsNotification };

0 commit comments

Comments
 (0)