@peopl-health/nexus 1.2.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION_GUIDE.md +71 -379
- package/README.md +80 -559
- package/lib/adapters/BaileysProvider.js +25 -1
- package/lib/adapters/TwilioProvider.js +107 -0
- package/lib/adapters/registry.js +29 -0
- package/lib/config/configLoader.js +38 -0
- package/lib/controllers/assistantController.js +6 -6
- package/lib/controllers/messageController.js +68 -21
- package/lib/controllers/templateController.js +5 -17
- package/lib/core/NexusMessaging.js +151 -35
- package/lib/helpers/assistantHelper.js +6 -6
- package/lib/helpers/baileysHelper.js +3 -3
- package/lib/helpers/twilioHelper.js +3 -3
- package/lib/index.d.ts +1 -1
- package/lib/index.js +84 -9
- package/lib/interactive/index.js +86 -0
- package/lib/interactive/registry.js +31 -0
- package/lib/interactive/twilioMapper.js +60 -0
- package/lib/models/agendaMessageModel.js +24 -0
- package/lib/models/messageModel.js +8 -0
- package/lib/routes/index.js +3 -1
- package/lib/services/airtableService.js +4 -0
- package/lib/services/assistantService.js +15 -2
- package/lib/storage/NoopStorage.js +19 -0
- package/lib/storage/registry.js +31 -0
- package/lib/templates/predefinedTemplates.js +1 -3
- package/lib/utils/defaultLLMProvider.js +1 -1
- package/lib/utils/index.js +3 -5
- package/package.json +2 -1
- package/lib/services/appointmentService.js +0 -55
|
@@ -14,7 +14,10 @@ class BaileysProvider extends MessageProvider {
|
|
|
14
14
|
|
|
15
15
|
async initialize() {
|
|
16
16
|
try {
|
|
17
|
-
const
|
|
17
|
+
const baileys = require('baileys');
|
|
18
|
+
// Support both CJS and ESM shapes
|
|
19
|
+
const makeWASocket = baileys.default || baileys.makeWASocket || baileys;
|
|
20
|
+
const useMultiFileAuthState = baileys.useMultiFileAuthState || baileys.useMultiFileAuthState;
|
|
18
21
|
const { useMongoDBAuthState } = require('../config/mongoAuthConfig');
|
|
19
22
|
const pino = require('pino');
|
|
20
23
|
|
|
@@ -175,6 +178,27 @@ class BaileysProvider extends MessageProvider {
|
|
|
175
178
|
}
|
|
176
179
|
this.isConnected = false;
|
|
177
180
|
}
|
|
181
|
+
|
|
182
|
+
// Content/Template operations are not supported for Baileys
|
|
183
|
+
async listTemplates() {
|
|
184
|
+
throw new Error('Template operations are only supported with Twilio provider');
|
|
185
|
+
}
|
|
186
|
+
async getTemplate() {
|
|
187
|
+
throw new Error('Template operations are only supported with Twilio provider');
|
|
188
|
+
}
|
|
189
|
+
async checkApprovalStatus() {
|
|
190
|
+
throw new Error('Template operations are only supported with Twilio provider');
|
|
191
|
+
}
|
|
192
|
+
async submitForApproval() {
|
|
193
|
+
throw new Error('Template operations are only supported with Twilio provider');
|
|
194
|
+
}
|
|
195
|
+
async deleteTemplate() {
|
|
196
|
+
throw new Error('Template operations are only supported with Twilio provider');
|
|
197
|
+
}
|
|
198
|
+
async createTemplate() {
|
|
199
|
+
throw new Error('Template operations are only supported with Twilio provider');
|
|
200
|
+
}
|
|
201
|
+
|
|
178
202
|
}
|
|
179
203
|
|
|
180
204
|
module.exports = { BaileysProvider };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { MessageProvider } = require('../core/MessageProvider');
|
|
2
|
+
const axios = require('axios');
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Twilio WhatsApp messaging provider
|
|
@@ -132,6 +133,112 @@ class TwilioProvider extends MessageProvider {
|
|
|
132
133
|
throw new Error(`Failed to list templates: ${error.message}`);
|
|
133
134
|
}
|
|
134
135
|
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Fetch a specific template/content by SID
|
|
139
|
+
*/
|
|
140
|
+
async getTemplate(sid) {
|
|
141
|
+
if (!this.isConnected || !this.twilioClient) {
|
|
142
|
+
throw new Error('Twilio provider not initialized');
|
|
143
|
+
}
|
|
144
|
+
if (!sid) throw new Error('Content SID is required');
|
|
145
|
+
try {
|
|
146
|
+
const content = await this.twilioClient.content.v1.contents(sid).fetch();
|
|
147
|
+
return content;
|
|
148
|
+
} catch (error) {
|
|
149
|
+
throw new Error(`Failed to get template: ${error.message}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Attempt to check approval status.
|
|
155
|
+
* Twilio SDK may not expose approval requests directly; return content and null approval by default.
|
|
156
|
+
*/
|
|
157
|
+
async checkApprovalStatus(sid) {
|
|
158
|
+
if (!sid) throw new Error('Content SID is required');
|
|
159
|
+
const content = await this.getTemplate(sid);
|
|
160
|
+
try {
|
|
161
|
+
const links = (content && content.links) || {};
|
|
162
|
+
let approvalsUrl = links.approvals || links.approvalRequests || links.approval_requests;
|
|
163
|
+
if (!approvalsUrl) {
|
|
164
|
+
approvalsUrl = 'https://content.twilio.com/v1/Content/' + sid + '/ApprovalRequests';
|
|
165
|
+
}
|
|
166
|
+
const resp = await axios.get(approvalsUrl, { auth: { username: this.accountSid, password: this.authToken } });
|
|
167
|
+
const data = resp && resp.data ? resp.data : null;
|
|
168
|
+
let approvals = [];
|
|
169
|
+
if (data) {
|
|
170
|
+
approvals = data.approval_requests || data.approvalRequests || data.results || data.data || (Array.isArray(data) ? data : []);
|
|
171
|
+
if (!Array.isArray(approvals) && data.approvalRequest) approvals = [data.approvalRequest];
|
|
172
|
+
}
|
|
173
|
+
const approvalRequest = Array.isArray(approvals) && approvals.length > 0 ? approvals[0] : null;
|
|
174
|
+
return { content, approvalRequest };
|
|
175
|
+
} catch (error) {
|
|
176
|
+
// If approval endpoint unavailable, return content only
|
|
177
|
+
return { content, approvalRequest: null, warning: error.message };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Submit template for approval (best-effort placeholder)
|
|
183
|
+
*/
|
|
184
|
+
async submitForApproval(contentSid, name, category) {
|
|
185
|
+
if (!contentSid) throw new Error('Content SID is required');
|
|
186
|
+
const content = await this.getTemplate(contentSid);
|
|
187
|
+
try {
|
|
188
|
+
const links = (content && content.links) || {};
|
|
189
|
+
let approvalsUrl = links.approvals || links.approvalRequests || links.approval_requests;
|
|
190
|
+
if (!approvalsUrl) {
|
|
191
|
+
approvalsUrl = 'https://content.twilio.com/v1/Content/' + contentSid + '/ApprovalRequests';
|
|
192
|
+
}
|
|
193
|
+
const payload = {
|
|
194
|
+
name,
|
|
195
|
+
category,
|
|
196
|
+
friendly_name: name,
|
|
197
|
+
categories: category ? [category] : undefined,
|
|
198
|
+
channel: 'whatsapp'
|
|
199
|
+
};
|
|
200
|
+
const resp = await axios.post(approvalsUrl, payload, { auth: { username: this.accountSid, password: this.authToken } });
|
|
201
|
+
return { success: true, contentSid, approvalRequest: resp.data || null };
|
|
202
|
+
} catch (error) {
|
|
203
|
+
throw new Error(`Failed to submit for approval: ${error.message}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Delete a template/content by SID
|
|
209
|
+
*/
|
|
210
|
+
async deleteTemplate(sid) {
|
|
211
|
+
if (!this.isConnected || !this.twilioClient) {
|
|
212
|
+
throw new Error('Twilio provider not initialized');
|
|
213
|
+
}
|
|
214
|
+
if (!sid) throw new Error('Content SID is required');
|
|
215
|
+
try {
|
|
216
|
+
await this.twilioClient.content.v1.contents(sid).remove();
|
|
217
|
+
return { success: true };
|
|
218
|
+
} catch (error) {
|
|
219
|
+
throw new Error(`Failed to delete template: ${error.message}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Create a template/content using Twilio Content API
|
|
225
|
+
* @param {Object} templateData - Must follow Twilio Content API schema
|
|
226
|
+
*/
|
|
227
|
+
async createTemplate(templateData) {
|
|
228
|
+
if (!this.isConnected || !this.twilioClient) {
|
|
229
|
+
throw new Error('Twilio provider not initialized');
|
|
230
|
+
}
|
|
231
|
+
if (!templateData || typeof templateData !== 'object') {
|
|
232
|
+
throw new Error('templateData must be an object');
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
const created = await this.twilioClient.content.v1.contents.create(templateData);
|
|
236
|
+
return created;
|
|
237
|
+
} catch (error) {
|
|
238
|
+
throw new Error(`Failed to create template: ${error.message}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
135
242
|
}
|
|
136
243
|
|
|
137
244
|
module.exports = { TwilioProvider };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const { TwilioProvider } = require('./TwilioProvider');
|
|
2
|
+
const { BaileysProvider } = require('./BaileysProvider');
|
|
3
|
+
|
|
4
|
+
const _providers = new Map();
|
|
5
|
+
|
|
6
|
+
function registerProvider(name, ProviderClass) {
|
|
7
|
+
if (!name || !ProviderClass) throw new Error('registerProvider requires name and ProviderClass');
|
|
8
|
+
_providers.set(String(name).toLowerCase(), ProviderClass);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getProvider(name) {
|
|
12
|
+
return _providers.get(String(name || '').toLowerCase());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function createProvider(name, config) {
|
|
16
|
+
const ProviderClass = getProvider(name);
|
|
17
|
+
if (!ProviderClass) throw new Error(`Unsupported provider: ${name}`);
|
|
18
|
+
return new ProviderClass(config || {});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Register built-ins
|
|
22
|
+
registerProvider('twilio', TwilioProvider);
|
|
23
|
+
registerProvider('baileys', BaileysProvider);
|
|
24
|
+
|
|
25
|
+
module.exports = {
|
|
26
|
+
registerProvider,
|
|
27
|
+
getProvider,
|
|
28
|
+
createProvider
|
|
29
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
const defaults = {
|
|
4
|
+
provider: { name: 'twilio', config: {} },
|
|
5
|
+
storage: null,
|
|
6
|
+
features: { airtable: true, s3: true },
|
|
7
|
+
assistants: { registry: {}, select: {}, getAssistantById: null },
|
|
8
|
+
handlers: {},
|
|
9
|
+
interactive: {}
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function merge(a, b) {
|
|
13
|
+
if (!b) return { ...a };
|
|
14
|
+
const out = { ...a };
|
|
15
|
+
for (const k of Object.keys(b)) {
|
|
16
|
+
if (b[k] && typeof b[k] === 'object' && !Array.isArray(b[k])) {
|
|
17
|
+
out[k] = merge(a[k] || {}, b[k]);
|
|
18
|
+
} else {
|
|
19
|
+
out[k] = b[k];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function loadNexusConfig(userConfig) {
|
|
26
|
+
if (userConfig && typeof userConfig === 'object') {
|
|
27
|
+
return merge(defaults, userConfig);
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const localPath = path.resolve(process.cwd(), 'nexus.config.js');
|
|
31
|
+
const fileConfig = require(localPath);
|
|
32
|
+
return merge(defaults, fileConfig || {});
|
|
33
|
+
} catch {
|
|
34
|
+
return { ...defaults };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { loadNexusConfig, defaults };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const { Config_ID } = require('../config/airtableConfig');
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const { Thread } = require('../models/threadModel');
|
|
4
4
|
|
|
5
5
|
const { getRecordByFilter } = require('../services/airtableService');
|
|
6
6
|
const { createAssistant, addMsgAssistant, addInsAssistant } = require('../services/assistantService');
|
|
@@ -12,7 +12,7 @@ const activeAssistantController = async (req, res) => {
|
|
|
12
12
|
const { code, active } = req.body;
|
|
13
13
|
|
|
14
14
|
try {
|
|
15
|
-
await
|
|
15
|
+
await Thread.updateOne({ code }, { $set: { active: !!active } });
|
|
16
16
|
return res.status(200).send({ message: 'Active assistant' });
|
|
17
17
|
} catch (error) {
|
|
18
18
|
console.log(error);
|
|
@@ -34,7 +34,7 @@ const addInsAssistantController = async (req, res) => {
|
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
const addMsgAssistantController = async (req, res) => {
|
|
37
|
-
const { code, messages, reply } = req.body;
|
|
37
|
+
const { code, messages, reply = false } = req.body;
|
|
38
38
|
|
|
39
39
|
try {
|
|
40
40
|
const ans = await addMsgAssistant(code, messages, reply);
|
|
@@ -47,7 +47,7 @@ const addMsgAssistantController = async (req, res) => {
|
|
|
47
47
|
};
|
|
48
48
|
|
|
49
49
|
const createAssistantController = async (req, res) => {
|
|
50
|
-
const { assistant_id, codes, messages=[], force=false } = req.body;
|
|
50
|
+
const { assistant_id, codes, instrucciones=[], messages=[], force=false } = req.body;
|
|
51
51
|
if (!Array.isArray(codes) || codes.length === 0) {
|
|
52
52
|
return res.status(400).send({ error: 'codes must be a non-empty array' });
|
|
53
53
|
}
|
|
@@ -62,7 +62,7 @@ const createAssistantController = async (req, res) => {
|
|
|
62
62
|
if (!force) continue;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
await createAssistant(code, assistant_id, messages, thread);
|
|
65
|
+
await createAssistant(code, assistant_id, [...instrucciones, ...messages], thread);
|
|
66
66
|
console.log('messages', messages);
|
|
67
67
|
for (const message of messages) {
|
|
68
68
|
console.log('message', message);
|
|
@@ -114,7 +114,7 @@ const stopAssistantController = async (req, res) => {
|
|
|
114
114
|
const { code, stop } = req.body;
|
|
115
115
|
|
|
116
116
|
try {
|
|
117
|
-
await
|
|
117
|
+
await Thread.updateOne({ code }, { $set: { stopped: !!stop } });
|
|
118
118
|
return res.status(200).send({ message: 'Stop assistant' });
|
|
119
119
|
} catch (error) {
|
|
120
120
|
console.log(error);
|
|
@@ -1,17 +1,21 @@
|
|
|
1
|
+
const { Message } = require('../models/messageModel.js');
|
|
2
|
+
|
|
1
3
|
// Import from Nexus core
|
|
2
4
|
const { sendMessage } = require('../core/NexusMessaging');
|
|
3
5
|
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
deleteOne: () => Promise.resolve({ success: false, error: 'Model not available' })
|
|
6
|
+
// Injectable dependencies with safe defaults
|
|
7
|
+
let injected = {
|
|
8
|
+
ScheduledMessage: null,
|
|
9
|
+
getRecordByFilter: null,
|
|
10
|
+
sendScheduledMessage: null
|
|
10
11
|
};
|
|
11
12
|
|
|
12
|
-
//
|
|
13
|
-
const getRecordByFilter =
|
|
14
|
-
|
|
13
|
+
// Allow consumers to inject their model and services
|
|
14
|
+
const configureMessageController = ({ ScheduledMessage, getRecordByFilter, sendScheduledMessage } = {}) => {
|
|
15
|
+
if (ScheduledMessage) injected.ScheduledMessage = ScheduledMessage;
|
|
16
|
+
if (getRecordByFilter) injected.getRecordByFilter = getRecordByFilter;
|
|
17
|
+
if (sendScheduledMessage) injected.sendScheduledMessage = sendScheduledMessage;
|
|
18
|
+
};
|
|
15
19
|
|
|
16
20
|
const moment = require('moment-timezone');
|
|
17
21
|
|
|
@@ -31,6 +35,9 @@ const sendMessageController = async (req, res) => {
|
|
|
31
35
|
const sendMoment = sendTime ? moment.tz(sendTime, timeZone) + 2500 : new Date();
|
|
32
36
|
|
|
33
37
|
try {
|
|
38
|
+
if (!injected.ScheduledMessage || typeof injected.ScheduledMessage.create !== 'function') {
|
|
39
|
+
return res.status(500).json({ success: false, error: 'ScheduledMessage model not configured. Call configureMessageController() to inject it.' });
|
|
40
|
+
}
|
|
34
41
|
const messageData = {
|
|
35
42
|
fileUrl,
|
|
36
43
|
message,
|
|
@@ -44,7 +51,7 @@ const sendMessageController = async (req, res) => {
|
|
|
44
51
|
extraDelay: 0,
|
|
45
52
|
variables
|
|
46
53
|
};
|
|
47
|
-
await ScheduledMessage.create(messageData);
|
|
54
|
+
await injected.ScheduledMessage.create(messageData);
|
|
48
55
|
console.log('Sending message with data:', messageData);
|
|
49
56
|
|
|
50
57
|
const result = await sendMessage(messageData);
|
|
@@ -76,12 +83,18 @@ const sendBulkMessageController = async (req, res) => {
|
|
|
76
83
|
const sendMoment = sendTime ? moment.tz(sendTime, timeZone) + 20*1000 : new Date();
|
|
77
84
|
|
|
78
85
|
try {
|
|
86
|
+
if (!injected.ScheduledMessage || typeof injected.ScheduledMessage.create !== 'function') {
|
|
87
|
+
return res.status(500).json({ success: false, error: 'ScheduledMessage model not configured. Call configureMessageController() to inject it.' });
|
|
88
|
+
}
|
|
89
|
+
if (!injected.sendScheduledMessage || typeof injected.sendScheduledMessage !== 'function') {
|
|
90
|
+
return res.status(500).json({ success: false, error: 'sendScheduledMessage not configured. Call configureMessageController() to inject it.' });
|
|
91
|
+
}
|
|
79
92
|
let numSend = 0;
|
|
80
93
|
let extraDelay = 0;
|
|
81
94
|
let curMessage = message;
|
|
82
95
|
const scheduledMessages = [];
|
|
83
96
|
for (const code of codes) {
|
|
84
|
-
const scheduledMessage = new ScheduledMessage({
|
|
97
|
+
const scheduledMessage = new injected.ScheduledMessage({
|
|
85
98
|
fileUrl,
|
|
86
99
|
message: curMessage,
|
|
87
100
|
fileType,
|
|
@@ -106,8 +119,8 @@ const sendBulkMessageController = async (req, res) => {
|
|
|
106
119
|
// Schedule all messages with Agenda in parallel
|
|
107
120
|
const sentMessages = await Promise.all(
|
|
108
121
|
scheduledMessages.map(async (message) => {
|
|
109
|
-
const savedMessage = await ScheduledMessage.create(message);
|
|
110
|
-
return sendScheduledMessage(savedMessage);
|
|
122
|
+
const savedMessage = await injected.ScheduledMessage.create(message);
|
|
123
|
+
return injected.sendScheduledMessage(savedMessage);
|
|
111
124
|
})
|
|
112
125
|
);
|
|
113
126
|
|
|
@@ -153,7 +166,16 @@ const sendBulkMessageAirtableController = async (req, res) => {
|
|
|
153
166
|
}
|
|
154
167
|
|
|
155
168
|
try {
|
|
156
|
-
|
|
169
|
+
if (!injected.getRecordByFilter || typeof injected.getRecordByFilter !== 'function') {
|
|
170
|
+
return res.status(500).json({ success: false, error: 'Airtable getRecordByFilter not configured. Call configureMessageController() to inject it.' });
|
|
171
|
+
}
|
|
172
|
+
if (!injected.ScheduledMessage || typeof injected.ScheduledMessage.create !== 'function') {
|
|
173
|
+
return res.status(500).json({ success: false, error: 'ScheduledMessage model not configured. Call configureMessageController() to inject it.' });
|
|
174
|
+
}
|
|
175
|
+
if (!injected.sendScheduledMessage || typeof injected.sendScheduledMessage !== 'function') {
|
|
176
|
+
return res.status(500).json({ success: false, error: 'sendScheduledMessage not configured. Call configureMessageController() to inject it.' });
|
|
177
|
+
}
|
|
178
|
+
const rows = await injected.getRecordByFilter(baseId, tableName, condition);
|
|
157
179
|
let extraDelay = 0;
|
|
158
180
|
let curMessage = message;
|
|
159
181
|
const sentPhones = new Set();
|
|
@@ -192,18 +214,14 @@ const sendBulkMessageAirtableController = async (req, res) => {
|
|
|
192
214
|
variables
|
|
193
215
|
};
|
|
194
216
|
|
|
195
|
-
// Add to scheduledMessages for both saving and scheduling
|
|
196
217
|
scheduledMessages.push(scheduledMessage);
|
|
197
|
-
|
|
198
|
-
// Increment delay and message counter
|
|
199
218
|
extraDelay += Math.floor(Math.random() * 5001) + 5000;
|
|
200
219
|
}
|
|
201
220
|
|
|
202
|
-
// Schedule all messages with Agenda in parallel
|
|
203
221
|
const sentMessages = await Promise.all(
|
|
204
222
|
scheduledMessages.map(async (message) => {
|
|
205
|
-
const savedMessage = await ScheduledMessage.create(message);
|
|
206
|
-
return sendScheduledMessage(savedMessage);
|
|
223
|
+
const savedMessage = await injected.ScheduledMessage.create(message);
|
|
224
|
+
return injected.sendScheduledMessage(savedMessage);
|
|
207
225
|
})
|
|
208
226
|
);
|
|
209
227
|
|
|
@@ -223,8 +241,37 @@ const sendBulkMessageAirtableController = async (req, res) => {
|
|
|
223
241
|
}
|
|
224
242
|
};
|
|
225
243
|
|
|
244
|
+
const getLastInteractionController = async (req, res) => {
|
|
245
|
+
const { code } = req.body;
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
const lastMessage = await Message.findOne({
|
|
249
|
+
$or: [
|
|
250
|
+
{ numero: code },
|
|
251
|
+
{ group_id: code }
|
|
252
|
+
]
|
|
253
|
+
}).sort({ createdAt: -1 }).exec();
|
|
254
|
+
|
|
255
|
+
if (!lastMessage) {
|
|
256
|
+
return res.status(404).send({ message: 'No messages found for the provided code.' });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const createdAt = new Date(lastMessage.createdAt);
|
|
260
|
+
const now = new Date();
|
|
261
|
+
const timeDiffMs = now - createdAt;
|
|
262
|
+
const minutes = Math.floor(timeDiffMs / (1000 * 60));
|
|
263
|
+
|
|
264
|
+
return res.status(200).send({ message: 'Last interaction retrieved successfully.', lastMessage, minutes });
|
|
265
|
+
} catch (error) {
|
|
266
|
+
console.error(error);
|
|
267
|
+
return res.status(500).send({ message: 'Failed to retrieve the last interaction.', error });
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
226
271
|
module.exports = {
|
|
227
272
|
sendMessageController,
|
|
228
273
|
sendBulkMessageController,
|
|
229
|
-
sendBulkMessageAirtableController
|
|
274
|
+
sendBulkMessageAirtableController,
|
|
275
|
+
getLastInteractionController,
|
|
276
|
+
configureMessageController
|
|
230
277
|
};
|
|
@@ -17,33 +17,21 @@ const checkTemplateSupport = () => {
|
|
|
17
17
|
throw new Error('Template operations are only supported with Twilio provider');
|
|
18
18
|
}
|
|
19
19
|
};
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
const { handleApiError } = require('../utils/errorHandler');
|
|
22
22
|
|
|
23
23
|
const { Template } = require('../templates/templateStructure');
|
|
24
24
|
const predefinedTemplates = require('../templates/predefinedTemplates');
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
const getTemplateModel = () => {
|
|
28
|
-
|
|
29
|
-
return mongoose.models.Template;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// If not in mongoose.models, require and return the model
|
|
28
|
+
// Require the concrete model; enforce presence instead of stubbing
|
|
33
29
|
try {
|
|
30
|
+
// If already registered in mongoose.models, require still returns the same model
|
|
34
31
|
const TemplateModel = require('../models/templateModel');
|
|
35
32
|
return TemplateModel;
|
|
36
33
|
} catch (error) {
|
|
37
|
-
|
|
38
|
-
// Return a stub model with required methods
|
|
39
|
-
return {
|
|
40
|
-
deleteMany: () => Promise.resolve({ deletedCount: 0 }),
|
|
41
|
-
find: () => ({ sort: () => ({ limit: () => ({ lean: () => Promise.resolve([]) }) }) }),
|
|
42
|
-
findOne: () => Promise.resolve(null),
|
|
43
|
-
create: () => Promise.resolve({}),
|
|
44
|
-
updateOne: () => Promise.resolve({}),
|
|
45
|
-
deleteOne: () => Promise.resolve({})
|
|
46
|
-
};
|
|
34
|
+
throw new Error('Template model not available. Ensure models are loaded before using template controllers.');
|
|
47
35
|
}
|
|
48
36
|
};
|
|
49
37
|
|