@peopl-health/nexus 3.1.0 → 3.1.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/lib/controllers/bugReportController.js +2 -2
- package/lib/helpers/messageStatusHelper.js +16 -7
- package/lib/helpers/processHelper.js +44 -1
- package/lib/helpers/templateRecoveryHelper.js +72 -0
- package/lib/models/messageModel.js +3 -0
- package/lib/routes/index.js +5 -0
- package/package.json +1 -1
|
@@ -46,8 +46,8 @@ const reportBugController = async (req, res) => {
|
|
|
46
46
|
|
|
47
47
|
if (!reporter) return res.status(400).json({ success: false, error: 'Reporter username is required' });
|
|
48
48
|
if (!whatsapp_id) return res.status(400).json({ success: false, error: 'WhatsApp ID is required' });
|
|
49
|
-
if (!severity || !['low', 'medium', 'high'].includes(severity)) {
|
|
50
|
-
return res.status(400).json({ success: false, error: 'Severity must be low, medium, or
|
|
49
|
+
if (!severity || !['low', 'medium', 'high', 'critical'].includes(severity)) {
|
|
50
|
+
return res.status(400).json({ success: false, error: 'Severity must be low, medium, high, or critical' });
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
logBugReportToAirtable(reporter, whatsapp_id, description, severity, messages).catch(err =>
|
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
const { Message } = require('../models/messageModel');
|
|
2
2
|
const { logger } = require('../utils/logger');
|
|
3
|
+
const { handle24HourWindowError } = require('./templateRecoveryHelper');
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Update message delivery status in the database based on Twilio status callback data
|
|
6
7
|
*/
|
|
7
8
|
async function updateMessageStatus(messageSid, status, errorCode = null, errorMessage = null) {
|
|
8
9
|
try {
|
|
9
|
-
const
|
|
10
|
-
status,
|
|
11
|
-
updatedAt: new Date()
|
|
10
|
+
const updateData = {
|
|
11
|
+
'statusInfo.status': status,
|
|
12
|
+
'statusInfo.updatedAt': new Date()
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
if (errorCode) {
|
|
15
|
-
statusInfo.errorCode = errorCode;
|
|
16
|
+
updateData['statusInfo.errorCode'] = errorCode;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
if (errorMessage) {
|
|
19
|
-
statusInfo.errorMessage = errorMessage;
|
|
20
|
+
updateData['statusInfo.errorMessage'] = errorMessage;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
const updated = await Message.findOneAndUpdate(
|
|
23
24
|
{ message_id: messageSid },
|
|
24
|
-
{ $set:
|
|
25
|
+
{ $set: updateData },
|
|
25
26
|
{ new: true }
|
|
26
27
|
);
|
|
27
28
|
|
|
@@ -62,12 +63,20 @@ async function handleStatusCallback(twilioStatusData) {
|
|
|
62
63
|
return null;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
|
|
66
|
+
const updated = await updateMessageStatus(
|
|
66
67
|
MessageSid,
|
|
67
68
|
MessageStatus.toLowerCase(),
|
|
68
69
|
ErrorCode || null,
|
|
69
70
|
ErrorMessage || null
|
|
70
71
|
);
|
|
72
|
+
|
|
73
|
+
if ((ErrorCode === 63016 || ErrorCode === '63016') && updated) {
|
|
74
|
+
handle24HourWindowError(updated, MessageSid).catch(err =>
|
|
75
|
+
logger.error('[MessageStatus] Recovery error', { messageSid: MessageSid, error: err.message })
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return updated;
|
|
71
80
|
}
|
|
72
81
|
|
|
73
82
|
/**
|
|
@@ -147,9 +147,41 @@ const processAudioFileCore = async (fileName, provider) => {
|
|
|
147
147
|
};
|
|
148
148
|
|
|
149
149
|
try {
|
|
150
|
+
const fileExtension = fileName.split('.').pop().toLowerCase();
|
|
151
|
+
const needsConversion = fileExtension === 'ogg'; // Convert OGG for OpenAI compatibility
|
|
152
|
+
let transcriptionFile = fileName;
|
|
153
|
+
let convertedFile = null;
|
|
154
|
+
|
|
155
|
+
if (needsConversion) {
|
|
156
|
+
logger.info('[processAudioFile] Converting OGG to MP3 for transcription', {
|
|
157
|
+
originalFile: fileName ? fileName.split('/').pop().replace(/^[^-]+-[^-]+-/, 'xxx-xxx-') : 'unknown',
|
|
158
|
+
format: fileExtension
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
convertedFile = fileName.replace('.ogg', '_converted.mp3');
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const { execSync } = require('child_process');
|
|
165
|
+
execSync(`ffmpeg -i "${fileName}" -acodec mp3 -ab 128k "${convertedFile}" -y`, {
|
|
166
|
+
stdio: 'pipe'
|
|
167
|
+
});
|
|
168
|
+
transcriptionFile = convertedFile;
|
|
169
|
+
|
|
170
|
+
logger.info('[processAudioFile] Conversion successful', {
|
|
171
|
+
convertedFile: convertedFile.split('/').pop().replace(/^[^-]+-[^-]+-/, 'xxx-xxx-')
|
|
172
|
+
});
|
|
173
|
+
} catch (conversionError) {
|
|
174
|
+
logger.error('[processAudioFile] Conversion failed, attempting with original file', {
|
|
175
|
+
error: conversionError.message,
|
|
176
|
+
originalFile: fileName ? fileName.split('/').pop().replace(/^[^-]+-[^-]+-/, 'xxx-xxx-') : 'unknown'
|
|
177
|
+
});
|
|
178
|
+
transcriptionFile = fileName;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
150
182
|
const { result: audioTranscript, duration: transcribeDuration } = await withTracing(
|
|
151
183
|
async () => provider.transcribeAudio({
|
|
152
|
-
file: fs.createReadStream(
|
|
184
|
+
file: fs.createReadStream(transcriptionFile),
|
|
153
185
|
responseFormat: 'text',
|
|
154
186
|
language: 'es'
|
|
155
187
|
}),
|
|
@@ -157,6 +189,17 @@ const processAudioFileCore = async (fileName, provider) => {
|
|
|
157
189
|
() => ({ 'audio.file_name': fileName ? fileName.split('/').pop().replace(/^[^-]+-[^-]+-/, 'xxx-xxx-') : 'unknown' }),
|
|
158
190
|
{ returnTiming: true }
|
|
159
191
|
)();
|
|
192
|
+
|
|
193
|
+
if (convertedFile && fs.existsSync(convertedFile)) {
|
|
194
|
+
try {
|
|
195
|
+
fs.unlinkSync(convertedFile);
|
|
196
|
+
logger.debug('[processAudioFile] Cleaned up converted file');
|
|
197
|
+
} catch (cleanupError) {
|
|
198
|
+
logger.warn('[processAudioFile] Failed to cleanup converted file', {
|
|
199
|
+
error: cleanupError.message
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
160
203
|
timings.transcribe_ms = transcribeDuration;
|
|
161
204
|
|
|
162
205
|
const transcriptText = audioTranscript?.text || audioTranscript;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const { logger } = require('../utils/logger');
|
|
2
|
+
const { getDefaultInstance } = require('../core/NexusMessaging');
|
|
3
|
+
const { Template, configureNexusProvider: configureTemplateProvider } = require('../templates/templateStructure');
|
|
4
|
+
const { sendMessage } = require('../core/NexusMessaging');
|
|
5
|
+
const { Message } = require('../models/messageModel');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Handle 24-hour window error by creating template and sending when approved
|
|
9
|
+
*/
|
|
10
|
+
async function handle24HourWindowError(message, messageSid) {
|
|
11
|
+
try {
|
|
12
|
+
if (!message?.body || !message?.numero) return;
|
|
13
|
+
|
|
14
|
+
const messaging = getDefaultInstance();
|
|
15
|
+
const provider = messaging?.getProvider();
|
|
16
|
+
if (!provider?.createTemplate) return;
|
|
17
|
+
|
|
18
|
+
configureTemplateProvider(provider);
|
|
19
|
+
|
|
20
|
+
const templateName = `auto_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
|
|
21
|
+
const template = new Template(templateName, 'UTILITY', 'es');
|
|
22
|
+
template.setBody(message.body, []);
|
|
23
|
+
const twilioContent = await template.save();
|
|
24
|
+
|
|
25
|
+
// Submit for approval
|
|
26
|
+
const approvalName = `${templateName}_${Date.now()}`.toLowerCase().replace(/[^a-z0-9_]/g, '_');
|
|
27
|
+
await provider.submitForApproval(twilioContent.sid, approvalName, 'UTILITY');
|
|
28
|
+
|
|
29
|
+
await Message.updateOne(
|
|
30
|
+
{ message_id: messageSid },
|
|
31
|
+
{ $set: { 'statusInfo.recoveryTemplateSid': twilioContent.sid } }
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
logger.info('[TemplateRecovery] Template created', { messageSid, templateSid: twilioContent.sid });
|
|
35
|
+
|
|
36
|
+
const checkApproval = async (attempt = 0, maxAttempts = 40) => {
|
|
37
|
+
if (attempt >= maxAttempts) {
|
|
38
|
+
logger.warn('[TemplateRecovery] Max attempts reached, template not approved yet', { messageSid, templateSid: twilioContent.sid });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
setTimeout(async () => {
|
|
43
|
+
try {
|
|
44
|
+
const status = await provider.checkApprovalStatus(twilioContent.sid);
|
|
45
|
+
const approvalStatus = status?.approvalRequest?.status?.toUpperCase();
|
|
46
|
+
|
|
47
|
+
if (approvalStatus === 'APPROVED') {
|
|
48
|
+
await sendMessage({ code: message.numero, contentSid: twilioContent.sid, variables: {} });
|
|
49
|
+
logger.info('[TemplateRecovery] Template sent', { messageSid, templateSid: twilioContent.sid });
|
|
50
|
+
} else if (approvalStatus === 'REJECTED') {
|
|
51
|
+
logger.warn('[TemplateRecovery] Template rejected', { messageSid, templateSid: twilioContent.sid });
|
|
52
|
+
} else {
|
|
53
|
+
checkApproval(attempt + 1, maxAttempts);
|
|
54
|
+
}
|
|
55
|
+
} catch (err) {
|
|
56
|
+
logger.error('[TemplateRecovery] Error checking approval', { error: err.message, attempt });
|
|
57
|
+
// Retry on error (but count as attempt)
|
|
58
|
+
if (attempt + 1 < maxAttempts) {
|
|
59
|
+
checkApproval(attempt + 1, maxAttempts);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}, 15 * 60 * 1000);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
checkApproval(0);
|
|
66
|
+
|
|
67
|
+
} catch (error) {
|
|
68
|
+
logger.error('[TemplateRecovery] Error', { messageSid, error: error.message });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { handle24HourWindowError };
|
|
@@ -124,6 +124,9 @@ async function insertMessage(values) {
|
|
|
124
124
|
updatedAt: values.delivery_status_updated_at || null
|
|
125
125
|
};
|
|
126
126
|
}
|
|
127
|
+
if (messageData.tools_executed === undefined) {
|
|
128
|
+
messageData.tools_executed = [];
|
|
129
|
+
}
|
|
127
130
|
|
|
128
131
|
await Message.findOneAndUpdate(
|
|
129
132
|
{ message_id: values.message_id, body: values.body },
|
package/lib/routes/index.js
CHANGED
|
@@ -39,6 +39,8 @@ const messageRouteDefinitions = {
|
|
|
39
39
|
'GET /last': 'getLastInteractionController',
|
|
40
40
|
'GET /scheduled-status': 'checkScheduledMessageStatusController',
|
|
41
41
|
'GET /status': 'checkMessageStatusController',
|
|
42
|
+
'GET /status/:messageSid': 'getMessageStatusController',
|
|
43
|
+
'POST /status-callback': 'messageStatusCallbackController',
|
|
42
44
|
'POST /quality': 'addQualityVoteController',
|
|
43
45
|
'GET /quality/:message_id': 'getQualityVotesByMessageController',
|
|
44
46
|
'GET /quality/:message_id/voter/:voter_username': 'getQualityVoteByMessageAndVoterController',
|
|
@@ -97,6 +99,7 @@ const conversationController = require('../controllers/conversationController');
|
|
|
97
99
|
const interactionController = require('../controllers/interactionController');
|
|
98
100
|
const mediaController = require('../controllers/mediaController');
|
|
99
101
|
const messageController = require('../controllers/messageController');
|
|
102
|
+
const messageStatusController = require('../controllers/messageStatusController');
|
|
100
103
|
const patientController = require('../controllers/patientController');
|
|
101
104
|
const qualityMessageController = require('../controllers/qualityMessageController');
|
|
102
105
|
const templateController = require('../controllers/templateController');
|
|
@@ -144,6 +147,8 @@ const builtInControllers = {
|
|
|
144
147
|
getLastInteractionController: messageController.getLastInteractionController,
|
|
145
148
|
checkScheduledMessageStatusController: messageController.checkScheduledMessageStatusController,
|
|
146
149
|
checkMessageStatusController: messageController.checkMessageStatusController,
|
|
150
|
+
getMessageStatusController: messageStatusController.getMessageStatusController,
|
|
151
|
+
messageStatusCallbackController: messageStatusController.messageStatusCallbackController,
|
|
147
152
|
addQualityVoteController: qualityMessageController.addQualityVoteController,
|
|
148
153
|
getQualityVotesByMessageController: qualityMessageController.getQualityVotesByMessageController,
|
|
149
154
|
getQualityVoteByMessageAndVoterController: qualityMessageController.getQualityVoteByMessageAndVoterController,
|