@peopl-health/nexus 1.1.4 → 1.1.6

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.
@@ -0,0 +1,134 @@
1
+ const { PDFDocument } = require('pdf-lib');
2
+ const { exec } = require('child_process');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const sharp = require('sharp');
6
+
7
+
8
+ async function convertPdfToImages(pdfName) {
9
+ const outputDir = path.join(__dirname, 'assets', 'tmp');
10
+ const pdfPath = `${outputDir}/${pdfName}.pdf`;
11
+
12
+ if (!fs.existsSync(outputDir)) {
13
+ fs.mkdirSync(outputDir, { recursive: true });
14
+ }
15
+
16
+ return new Promise((resolve, reject) => {
17
+ const command = `pdftoppm -jpeg ${pdfPath} ${outputDir}/${pdfName}`;
18
+ console.log(command);
19
+ exec(command, (error, stdout, stderr) => {
20
+ if (error) {
21
+ return reject(`Error splitting PDF: ${stderr}`);
22
+ }
23
+
24
+ fs.readdir(outputDir, (err, files) => {
25
+ if (err) {
26
+ return reject(`Error reading output directory: ${err.message}`);
27
+ }
28
+
29
+ const pngFiles = files
30
+ .filter(file => file.startsWith(pdfName) && file.endsWith('.jpg'))
31
+ .map(file => path.join(outputDir, file));
32
+
33
+ resolve(pngFiles);
34
+ });
35
+ });
36
+ });
37
+ }
38
+
39
+ async function combineImagesToPDF(config) {
40
+ const {
41
+ code,
42
+ extensions = ['jpg', 'jpeg', 'png', 'tiff'],
43
+ sortNumerically = true
44
+ } = config;
45
+
46
+ // Get all files in the directory
47
+ const inputDir = path.join(__dirname, 'assets', 'tmp');
48
+ const files = await fs.promises.readdir(inputDir);
49
+
50
+ // Filter for image files with the specified extensions
51
+ const imageFiles = files.filter(file => {
52
+ const ext = path.extname(file).toLowerCase().substring(1);
53
+ const hasValidExtension = extensions.includes(ext);
54
+ const hasPrefix = code ? file.startsWith(code) : true;
55
+ return hasValidExtension && hasPrefix;
56
+ });
57
+
58
+ // Sort files
59
+ if (sortNumerically) {
60
+ imageFiles.sort((a, b) => {
61
+ const aMatch = a.match(/\d+/g);
62
+ const bMatch = b.match(/\d+/g);
63
+
64
+ if (!aMatch || !bMatch) return a.localeCompare(b);
65
+
66
+ return parseInt(aMatch[0]) - parseInt(bMatch[0]);
67
+ });
68
+ } else {
69
+ imageFiles.sort();
70
+ }
71
+
72
+ console.log(`Found ${imageFiles.length} image files to combine`);
73
+
74
+ // Create a new PDF document
75
+ const pdfDoc = await PDFDocument.create();
76
+ const processedFiles = [];
77
+
78
+ // Process each image file
79
+ for (const [index, file] of imageFiles.entries()) {
80
+ try {
81
+ const filePath = path.join(inputDir, file);
82
+ console.log(`Processing file ${index + 1}/${imageFiles.length}: ${file}`);
83
+
84
+ const imageBuffer = await fs.promises.readFile(filePath);
85
+ const pngBuffer = await sharp(imageBuffer)
86
+ .toFormat('png')
87
+ .toBuffer();
88
+
89
+ const { width, height } = await sharp(imageBuffer).metadata();
90
+ const img = await pdfDoc.embedPng(pngBuffer);
91
+ const page = pdfDoc.addPage([width, height]);
92
+
93
+ // Draw the image on the page
94
+ page.drawImage(img, {
95
+ x: 0,
96
+ y: 0,
97
+ width: width,
98
+ height: height,
99
+ });
100
+
101
+ processedFiles.push(filePath);
102
+ } catch (error) {
103
+ console.error(`Error processing file ${file}:`, error);
104
+ }
105
+ }
106
+
107
+ console.log('Combined PDF created successfully');
108
+
109
+ return {
110
+ pdfBuffer: await pdfDoc.save(),
111
+ processedFiles
112
+ };
113
+ }
114
+
115
+ function cleanupFiles(files) {
116
+ files.forEach(file => {
117
+ if (fs.existsSync(file)) {
118
+ try {
119
+ fs.unlinkSync(file);
120
+ console.log(`Deleted: ${file}`);
121
+ } catch (error) {
122
+ console.error(`Error deleting ${file}:`, error);
123
+ }
124
+ } else {
125
+ console.warn(`File not found: ${file}`);
126
+ }
127
+ });
128
+ }
129
+
130
+ module.exports = {
131
+ convertPdfToImages,
132
+ combineImagesToPDF,
133
+ cleanupFiles
134
+ };
@@ -0,0 +1,202 @@
1
+ const { anthropicClient } = require('../config/llmConfig.js');
2
+ const fs = require('fs');
3
+ const mime = require('mime-types');
4
+
5
+
6
+ async function analyzeImage(imagePath) {
7
+ try {
8
+ // Skip WBMP images and stickers
9
+ if (imagePath.toLowerCase().includes('.wbmp') || imagePath.toLowerCase().includes('sticker')) {
10
+ console.log('Skipping WBMP image or sticker analysis:', imagePath);
11
+ return {
12
+ medical_analysis: 'NOT_MEDICAL',
13
+ medical_relevance: false,
14
+ has_table: false,
15
+ table_data: null
16
+ };
17
+ }
18
+
19
+ // Check MIME type
20
+ const mimeType = mime.lookup(imagePath) || 'image/jpeg';
21
+ if (mimeType === 'image/vnd.wap.wbmp' || mimeType.includes('sticker')) {
22
+ console.log('Skipping image with MIME type:', mimeType);
23
+ return {
24
+ medical_analysis: 'NOT_MEDICAL',
25
+ medical_relevance: false,
26
+ has_table: false,
27
+ table_data: null
28
+ };
29
+ }
30
+ // Read the image file and convert to base64
31
+ const imageBuffer = await fs.promises.readFile(imagePath);
32
+ const base64Image = imageBuffer.toString('base64');
33
+
34
+ // Create a more specific prompt for table detection and extraction
35
+ const tablePrompt = `Please analyze this image and respond in the following format:
36
+ 1. First, determine if there is a table in the image.
37
+ 2. If there is NO table, respond with exactly "NONE"
38
+ 3. If there IS a table:
39
+ - Extract all content from the table
40
+ - Format it as a markdown table
41
+ - Include all headers and data
42
+ - Preserve the exact text and numbers as shown
43
+ - Maintain the original column order
44
+ - Pay special attention to how values align vertically with test names in the original document
45
+ - Double-check each value's vertical alignment with its corresponding test name before entering it in the table
46
+ - Include all units and reference ranges exactly as written
47
+ - Double-check each value-to-test pairing before including it
48
+ - Do not summarize or modify the data
49
+
50
+ Only extract tables - ignore any other content in the image.`;
51
+
52
+ // Create the message with the image
53
+ const messageTable = await anthropicClient.messages.create({
54
+ model: 'claude-3-7-sonnet-20250219',
55
+ max_tokens: 1024,
56
+ messages: [
57
+ {
58
+ role: 'user',
59
+ content: [
60
+ {
61
+ type: 'image',
62
+ source: {
63
+ type: 'base64',
64
+ media_type: mime.lookup(imagePath) || 'image/jpeg',
65
+ data: base64Image,
66
+ },
67
+ },
68
+ {
69
+ type: 'text',
70
+ text: tablePrompt,
71
+ },
72
+ ],
73
+ },
74
+ ],
75
+ });
76
+
77
+ // Create a more specific prompt for table detection and extraction
78
+ const medImagePrompt = `
79
+ Eres un oncólogo clínico con experiencia. Se te proporcionará una imagen médica o laboratorio. Analízala y responde exactamente en este formato:
80
+
81
+ 1. Image Type:
82
+ - Especifica tipo de imagen médico (ej. X‑ray, CT, MRI, ecografía, biopsia digital, resultados de laboratorio, etc.) o "NOT_MEDICAL" si no es médica.
83
+ - Calidad de la imagen: clear / unclear / incomplete.
84
+
85
+ 2. Clinical Analysis:
86
+ - Hallazgos clave y anomalías.
87
+ - Estructuras normales significativas presentes.
88
+ - Medidas críticas (diámetros, densidades, volúmenes), si aplican.
89
+
90
+ 3. Action Items:
91
+ - Pruebas o vistas adicionales recomendadas.
92
+ - Limitaciones clave de la evaluación actual.
93
+
94
+ Si la imagen no permite ningún análisis útil, responde solo con: QUALITY_INSUFFICIENT.
95
+
96
+ ---
97
+
98
+ <OUTPUT_FORMAT>
99
+ 1. Image Type:
100
+ - ...
101
+ 2. Clinical Analysis:
102
+ - ...
103
+ 3. Action Items:
104
+ - ...
105
+ </OUTPUT_FORMAT>
106
+
107
+ <EJEMPLOS>
108
+ Ejemplo 1:
109
+ (Descripción breve si compartieras una RX de tórax)
110
+ 1. Image Type:
111
+ - X‑ray de tórax PA, clear
112
+ 2. Clinical Analysis:
113
+ - Líneas pleurales visibles, silueta cardíaca normal…
114
+ 3. Action Items:
115
+ - Sugiero proyección lateral para valorar consolidación…
116
+ </EJEMPLOS>
117
+ `;
118
+
119
+ // Create the message with the image
120
+ const messageMedImage = await anthropicClient.messages.create({
121
+ model: 'claude-3-7-sonnet-20250219',
122
+ max_tokens: 1024,
123
+ messages: [
124
+ {
125
+ role: 'user',
126
+ content: [
127
+ {
128
+ type: 'image',
129
+ source: {
130
+ type: 'base64',
131
+ media_type: mime.lookup(imagePath) || 'image/jpeg',
132
+ data: base64Image,
133
+ },
134
+ },
135
+ {
136
+ type: 'text',
137
+ text: medImagePrompt,
138
+ },
139
+ ],
140
+ },
141
+ ],
142
+ });
143
+
144
+ const relevancePrompt = `Please analyze this image and respond in this format:
145
+ Medical Relevance: [YES/NO]`;
146
+
147
+ // Create the message with the image
148
+ const messageRelevance = await anthropicClient.messages.create({
149
+ model: 'claude-3-7-sonnet-20250219',
150
+ max_tokens: 1024,
151
+ messages: [
152
+ {
153
+ role: 'user',
154
+ content: [
155
+ {
156
+ type: 'image',
157
+ source: {
158
+ type: 'base64',
159
+ media_type: mime.lookup(imagePath) || 'image/jpeg',
160
+ data: base64Image,
161
+ },
162
+ },
163
+ {
164
+ type: 'text',
165
+ text: relevancePrompt,
166
+ },
167
+ ],
168
+ },
169
+ ],
170
+ });
171
+
172
+ const messageTableStr = messageTable.content[0].text;
173
+ const messageRelevanceStr = messageRelevance.content[0].text;
174
+ const messageAnalysisStr = messageMedImage.content[0].text;
175
+ const table = (messageTableStr.trim() === 'NONE') ? null : messageTableStr.trim();
176
+ const isTable = (table === null) ? false : true;
177
+ const isRelevant = (messageRelevanceStr.includes('YES')) ? true : false;
178
+
179
+ return {medical_analysis: messageAnalysisStr, medical_relevance: isRelevant,
180
+ has_table: isTable, table_data: table};
181
+ } catch (error) {
182
+ console.error('Error analyzing image:', error);
183
+ throw error;
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Helper to build prompts by replacing context data in template files
189
+ * @param {Object} conversationContext - Context object to inject into prompt
190
+ * @param {string} promptPath - Path to the prompt template file
191
+ * @returns {string} Formatted prompt with context data injected
192
+ */
193
+ const buildPrompt = (conversationContext, promptPath) => {
194
+ const promptTemplate = fs.readFileSync(promptPath, 'utf8');
195
+
196
+ return promptTemplate.replace('{{CONVERSATION_DATA}}', JSON.stringify(conversationContext, null, 2));
197
+ };
198
+
199
+ module.exports = {
200
+ analyzeImage,
201
+ buildPrompt
202
+ };
@@ -0,0 +1,72 @@
1
+ const AWS = require('../config/awsConfig.js');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+
5
+ async function uploadMediaToS3(buffer, messageID, titleFile, bucketName, contentType, messageType) {
6
+ const extension = getFileExtension(contentType);
7
+ const sanitizedTitle = titleFile ? sanitizeFileName(titleFile) : '';
8
+ const fileName = sanitizedTitle
9
+ ? `${messageType}/${messageID}_${sanitizedTitle}.${extension}`
10
+ : `${messageType}/${messageID}.${extension}`;
11
+ console.log(titleFile, messageType);
12
+
13
+ try {
14
+ await AWS.uploadBufferToS3(buffer, bucketName, fileName, contentType);
15
+ return fileName;
16
+ } catch (error) {
17
+ console.error('Failed to upload media to S3:', error.stack);
18
+ throw error;
19
+ }
20
+ }
21
+
22
+ async function saveMediaToLocal(mediaBuffer, messageId, contentType) {
23
+ const uploadDir = path.join(__dirname, '../uploads');
24
+
25
+ if (!fs.existsSync(uploadDir)) {
26
+ fs.mkdirSync(uploadDir, { recursive: true });
27
+ }
28
+
29
+ const fileExt = getFileExtension(contentType);
30
+ const filePath = path.join(uploadDir, `${messageId}.${fileExt}`);
31
+
32
+ return new Promise((resolve, reject) => {
33
+ fs.writeFile(filePath, mediaBuffer, (err) => {
34
+ if (err) reject(err);
35
+ else resolve(filePath);
36
+ });
37
+ });
38
+ }
39
+
40
+ function getFileExtension(contentType) {
41
+ const mimeToExt = {
42
+ 'image/jpeg': 'jpg',
43
+ 'image/png': 'png',
44
+ 'image/gif': 'gif',
45
+ 'image/webp': 'webp',
46
+ 'audio/mp4': 'm4a',
47
+ 'audio/mpeg': 'mp3',
48
+ 'audio/ogg': 'ogg',
49
+ 'video/mp4': 'mp4',
50
+ 'video/3gpp': '3gp',
51
+ 'application/pdf': 'pdf',
52
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx',
53
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx',
54
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'pptx',
55
+ 'text/plain': 'txt'
56
+ };
57
+
58
+ return mimeToExt[contentType] || 'bin';
59
+ }
60
+
61
+ function sanitizeFileName(fileName) {
62
+ return fileName
63
+ .replace(/[^a-zA-Z0-9_-]/g, '')
64
+ .slice(0, 50);
65
+ }
66
+
67
+ module.exports = {
68
+ uploadMediaToS3,
69
+ saveMediaToLocal,
70
+ getFileExtension,
71
+ sanitizeFileName
72
+ };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Helper to convert date strings to Date objects in MongoDB filters
3
+ * @param {Object} filter - Filter that may contain date strings
4
+ * @returns {Object} Filter with dates converted to Date objects
5
+ */
6
+ const convertDateStringsInFilter = (filter) => {
7
+ if (!filter || typeof filter !== 'object') {
8
+ return filter;
9
+ }
10
+
11
+ const convertedFilter = { ...filter };
12
+
13
+ // Recursive function to convert dates
14
+ const convertDates = (obj) => {
15
+ for (const key in obj) {
16
+ if (typeof obj[key] === 'object' && obj[key] !== null) {
17
+ if (obj[key].$gte || obj[key].$gt || obj[key].$lte || obj[key].$lt) {
18
+ // Convert date comparison operators
19
+ if (obj[key].$gte && typeof obj[key].$gte === 'string') {
20
+ obj[key].$gte = new Date(obj[key].$gte);
21
+ }
22
+ if (obj[key].$gt && typeof obj[key].$gt === 'string') {
23
+ obj[key].$gt = new Date(obj[key].$gt);
24
+ }
25
+ if (obj[key].$lte && typeof obj[key].$lte === 'string') {
26
+ obj[key].$lte = new Date(obj[key].$lte);
27
+ }
28
+ if (obj[key].$lt && typeof obj[key].$lt === 'string') {
29
+ obj[key].$lt = new Date(obj[key].$lt);
30
+ }
31
+ } else {
32
+ // Recursion for nested objects
33
+ convertDates(obj[key]);
34
+ }
35
+ }
36
+ }
37
+ };
38
+
39
+ convertDates(convertedFilter);
40
+ return convertedFilter;
41
+ };
42
+
43
+ module.exports = {
44
+ convertDateStringsInFilter
45
+ };
@@ -0,0 +1,22 @@
1
+ const qrcode = require('qrcode');
2
+
3
+
4
+ async function generateQRBuffer(text) {
5
+ try {
6
+ const qrBuffer = await qrcode.toBuffer(text, { type: 'image/png' });
7
+ return qrBuffer;
8
+ } catch (err) {
9
+ console.error('Error generating QR code:', err.stack);
10
+ throw err;
11
+ }
12
+ }
13
+
14
+ function bufferToBase64(buffer) {
15
+ return buffer.toString('base64');
16
+ }
17
+
18
+
19
+ module.exports = {
20
+ generateQRBuffer,
21
+ bufferToBase64
22
+ };
@@ -0,0 +1,138 @@
1
+ const { LegacyMessage } = require('../models/messageModel');
2
+
3
+ const axios = require('axios');
4
+ const { v4: uuidv4 } = require('uuid');
5
+
6
+
7
+ function convertTwilioToInternalFormat(twilioMessage) {
8
+ const from = twilioMessage.From || '';
9
+ const to = twilioMessage.To || '';
10
+ const fromMe = to === from;
11
+
12
+ return {
13
+ key: {
14
+ id: twilioMessage.MessageSid || uuidv4(),
15
+ fromMe: fromMe,
16
+ remoteJid: fromMe ? to : from
17
+ },
18
+ pushName: twilioMessage.ProfileName || '',
19
+ message: {
20
+ conversation: twilioMessage.Body || ''
21
+ },
22
+ messageTimestamp: Math.floor(Date.now() / 1000)
23
+ };
24
+ }
25
+
26
+
27
+ async function downloadMediaFromTwilio(mediaUrl, logger) {
28
+ try {
29
+ logger.info(`Downloading media from: ${mediaUrl}`);
30
+
31
+ const response = await axios({
32
+ method: 'GET',
33
+ url: mediaUrl,
34
+ responseType: 'arraybuffer',
35
+ headers: {
36
+ 'Authorization': `Basic ${Buffer.from(
37
+ `${process.env.TWILIO_ACCOUNT_SID}:${process.env.TWILIO_AUTH_TOKEN}`
38
+ ).toString('base64')}`
39
+ }
40
+ });
41
+
42
+ return Buffer.from(response.data);
43
+ } catch (error) {
44
+ logger.error(`Failed to download media: ${error.message}`);
45
+ throw error;
46
+ }
47
+ }
48
+
49
+
50
+ function getMediaTypeFromContentType(contentType) {
51
+ if (contentType.startsWith('image/')) return 'imageMessage';
52
+ if (contentType.startsWith('audio/')) return 'audioMessage';
53
+ if (contentType.startsWith('video/')) return 'videoMessage';
54
+ if (contentType.startsWith('application/')) return 'documentMessage';
55
+ return 'unknownMessage';
56
+ }
57
+
58
+
59
+ function extractTitle(message, mediaType) {
60
+ if (mediaType === 'documentMessage' && message.MediaUrl0) {
61
+ const urlParts = message.MediaUrl0.split('/');
62
+ return urlParts[urlParts.length - 1] || null;
63
+ }
64
+ return null;
65
+ }
66
+
67
+
68
+ async function isRecentMessage(chatId) {
69
+ const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
70
+
71
+ const recentMessage = await LegacyMessage.find({
72
+ $or: [{ group_id: chatId }, { numero: chatId }],
73
+ timestamp: { $gte: fiveMinutesAgo.toISOString() }
74
+ }).sort({ timestamp: -1 }).limit(1);
75
+
76
+ return !!recentMessage;
77
+ }
78
+
79
+
80
+ async function getLastMessages(chatId, n) {
81
+ const messages = await LegacyMessage.find({ numero: chatId })
82
+ .sort({ timestamp: -1 })
83
+ .limit(n)
84
+ .select('timestamp numero nombre_whatsapp body');
85
+
86
+ return messages.map(msg => `[${msg.timestamp}] ${msg.body}`);
87
+ }
88
+
89
+
90
+ async function downloadMedia(twilioMessage, logger) {
91
+ try {
92
+ const mediaUrl = twilioMessage.MediaUrl0;
93
+ if (!mediaUrl) {
94
+ throw new Error('No media URL provided');
95
+ }
96
+
97
+ logger.info(`Downloading media from: ${mediaUrl}`);
98
+
99
+ const response = await axios({
100
+ method: 'GET',
101
+ url: mediaUrl,
102
+ responseType: 'arraybuffer',
103
+ headers: {
104
+ 'Authorization': `Basic ${Buffer.from(
105
+ `${process.env.TWILIO_ACCOUNT_SID}:${process.env.TWILIO_AUTH_TOKEN}`
106
+ ).toString('base64')}`
107
+ }
108
+ });
109
+
110
+ return Buffer.from(response.data);
111
+ } catch (error) {
112
+ logger.error(`Error downloading media: ${error.message}`);
113
+ throw error;
114
+ }
115
+ }
116
+
117
+
118
+ const ensureWhatsAppFormat = (phoneNumber) => {
119
+ if (!phoneNumber) return null;
120
+
121
+ if (phoneNumber.startsWith('whatsapp:')) {
122
+ return phoneNumber;
123
+ }
124
+
125
+ return `whatsapp:${phoneNumber}`;
126
+ };
127
+
128
+
129
+ module.exports = {
130
+ convertTwilioToInternalFormat,
131
+ downloadMediaFromTwilio,
132
+ getMediaTypeFromContentType,
133
+ extractTitle,
134
+ isRecentMessage,
135
+ getLastMessages,
136
+ downloadMedia,
137
+ ensureWhatsAppFormat
138
+ };
@@ -0,0 +1,75 @@
1
+ const moment = require('moment-timezone');
2
+
3
+
4
+ function delay(ms) {
5
+ return new Promise(resolve => setTimeout(resolve, ms));
6
+ }
7
+
8
+
9
+ function formatCode(codeBase) {
10
+ console.log(`formatCode ${codeBase}`);
11
+
12
+ const [number, domain] = codeBase.split('@');
13
+
14
+ if (!number || !domain) {
15
+ throw new Error('Invalid code format: missing number or domain');
16
+ }
17
+
18
+ if (domain !== 's.whatsapp.net') {
19
+ throw new Error('Invalid domain: must be s.whatsapp.net');
20
+ }
21
+
22
+ let formattedNumber = number;
23
+
24
+ if (formattedNumber.endsWith('-2')) {
25
+ formattedNumber = formattedNumber.slice(0, -2);
26
+ }
27
+
28
+ if (formattedNumber.length === 10) {
29
+ formattedNumber = '52' + formattedNumber;
30
+ }
31
+
32
+ if (formattedNumber.startsWith('52') && formattedNumber.length === 12) {
33
+ formattedNumber = formattedNumber.substring(0, 2) + '1' + formattedNumber.substring(2);
34
+ }
35
+ return `${formattedNumber}@s.whatsapp.net`;
36
+ }
37
+
38
+
39
+ function calculateDelay(sendTime, timeZone) {
40
+ if (sendTime !== undefined && timeZone !== undefined) {
41
+ const sendMoment = moment.tz(sendTime, timeZone);
42
+
43
+ if (!sendMoment.isValid()) {
44
+ return { error: 'Invalid time format' };
45
+ }
46
+
47
+ // Get the current time and calculate the difference
48
+ const now = moment().tz(timeZone);
49
+ const randomDelay = Math.floor(Math.random() * 15001) + 15000;
50
+ const delay = sendMoment.diff(now) + randomDelay;
51
+
52
+ // Log the calculated details for debugging
53
+ console.log(
54
+ 'Scheduled Time:', sendMoment.format(),
55
+ 'Current Time:', now.format(),
56
+ 'Delay (minutes):', delay / 60000,
57
+ 'Remaining Seconds:', delay % 60000
58
+ );
59
+
60
+ if (delay <= 0) {
61
+ return 2500;
62
+ }
63
+
64
+ return delay;
65
+ } else {
66
+ return 2500;
67
+ }
68
+ }
69
+
70
+
71
+ module.exports = {
72
+ delay,
73
+ formatCode,
74
+ calculateDelay
75
+ };