From 5e0a02fb58193e8e8f4c4504f111d6dac2aa4388 Mon Sep 17 00:00:00 2001 From: Abhinav Sinha Date: Sun, 9 May 2021 18:03:21 +0530 Subject: [PATCH 1/2] introducing welcome email per issue#1 --- src/app.js | 50 ++++++++++++++++++++-------------- src/configs/mailConfig.js | 7 +++-- src/configs/schedulerConfig.js | 2 +- src/services/alerts.js | 4 +-- src/utilities/appointments.js | 2 +- src/utilities/htmlBuilder.js | 40 +++++++++++++++++++++++++-- 6 files changed, 77 insertions(+), 28 deletions(-) diff --git a/src/app.js b/src/app.js index 2b05076..e8839ca 100644 --- a/src/app.js +++ b/src/app.js @@ -3,6 +3,7 @@ const moment = require('moment'); const cron = require('node-cron'); const axios = require('axios'); const appConfig = require('./configs/appConfig'); +const mailConfig = require('./configs/mailConfig'); const schedulerConfig = require('./configs/schedulerConfig'); const alerts = require('./services/alerts'); const routes = require('./api/routes'); @@ -10,13 +11,15 @@ const locations = require('./utilities/locations'); const appointments = require('./utilities/appointments'); const htmlBuilder = require('./utilities/htmlBuilder'); +let runCounter = 0; let vaccinationSlots = []; async function main() { try { - cron.schedule(schedulerConfig.SCHEDULE, async () => { - await fetchVaccinationSlots(); - }); + await fetchVaccinationSlots(); + // cron.schedule(schedulerConfig.SCHEDULE, async () => { + // await fetchVaccinationSlots(); + // }); } catch (e) { console.log('There was an error fetching vaccine slots: ' + JSON.stringify(e, null, 2)); throw e; @@ -32,20 +35,20 @@ async function fetchVaccinationSlots() { let pincodeArray = appConfig.PINCODE.split(','); for( counter = 0; counter < pincodeArray.length; counter++) { if (pincodeArray[counter].toString().length < 6) { - console.log('Invalid pincode ' + pincodeArray[counter] + ' provided in config file: (src/config/appConfig.js)'); + console.log('Invalid pincode ' + pincodeArray[counter] + ' provided in config file: (src/config/appConfig.js).'); pincodeArray.splice(counter, 1); } } getAppointmentsByPincode(pincodeArray, dates); } else if (appConfig.PINCODE.toString().length < 6) { - throw 'Application is set to fetch vaccine slots by pincode but no pincode/ invalid pincode provided in config file: (src/config/appConfig.js)'; + throw 'Application is set to fetch vaccine slots by pincode but no pincode/ invalid pincode provided in config file: (src/config/appConfig.js).'; } else { pincodeArray.push(appConfig.PINCODE); getAppointmentsByPincode(pincodeArray, dates); } } else { if (appConfig.DISTRICT.length == 0) { - throw 'Application is set to fetch vaccine slots by district but no district name provided in config file: (src/config/appConfig.js)'; + throw 'Application is set to fetch vaccine slots by district but no district name provided in config file: (src/config/appConfig.js).'; } getAppointmentsByDistrict(dates); } @@ -77,9 +80,9 @@ async function getAppointmentsByDistrict(dates) { let stateId = await locations.getStateId(appConfig.STATE); let districtId = await locations.getDistrictId(stateId, appConfig.DISTRICT); if (stateId == undefined) { - throw 'Unable to find state id. Please verify state name in config file: src/config/appConfig.js'; + throw 'Unable to find state id. Please verify state name in config file: src/config/appConfig.js.'; } else if (districtId == undefined) { - throw 'Unable to find district id. Please verify district name in config file: src/config/appConfig.js'; + throw 'Unable to find district id. Please verify district name in config file: src/config/appConfig.js.'; } for await (const date of dates) { let slots = await routes.getVaccinationSlotsByDistrict(districtId, date) @@ -110,28 +113,35 @@ async function getTwoWeekDateArray() { async function sendEmailAlert(slotsArray) { let outputArray = []; - if (slotsArray[0] == undefined) { - console.log('No sessions to process at this time'); - } else { - for(let counter1 = 0; counter1 < slotsArray.length; counter1++) { + for(let counter1 = 0; counter1 < slotsArray.length; counter1++) { + if (slotsArray[counter1] != undefined || slotsArray[counter1] != undefined && slotsArray[counter1].length > 0) { for(let counter2 = 0; counter2 < slotsArray[counter1].length; counter2++) { outputArray.push(slotsArray[counter1][counter2]); } } - if (Boolean(await appointments.compareVaccinationSlots(outputArray, vaccinationSlots))) { - console.log('No new sessions to process at this time'); - } else { - let htmlBody = await htmlBuilder.prepareHtmlBody(outputArray); - vaccinationSlots = outputArray; - alerts.sendEmailAlert(htmlBody, (err, result) => { + } + if (outputArray.length > 0 && !Boolean(await appointments.compareVaccinationSlots(outputArray, vaccinationSlots))) { + vaccinationSlots = outputArray; + alerts.sendEmailAlert(mailConfig.SUBJECT, await htmlBuilder.prepareHtmlBody(outputArray), (err, result) => { + if(err) { + console.error({err}); + } else { + console.log('New sessions have been processed and sent as an email alert to recipient(s).'); + } + }); + } else { + console.log('No new sessions to process at this time.'); + if (runCounter == 0) { + alerts.sendEmailAlert(mailConfig.FIRST_EMAIL_SUBJECT, await htmlBuilder.prepareFirstEmail(), (err, result) => { if(err) { console.error({err}); } else { - console.log('New sessions have been processed and sent as an email alert'); + console.log('Welcome email alert has been sent to recipient(s).'); } - }) + }); } } + runCounter++; }; main().then(() => { diff --git a/src/configs/mailConfig.js b/src/configs/mailConfig.js index bb93e90..b0e5eea 100644 --- a/src/configs/mailConfig.js +++ b/src/configs/mailConfig.js @@ -2,6 +2,9 @@ const SERVICE_PROVIDER = 'Gmail'; const RECIPIENT = 'asinha093@gmail.com'; const SENDER = 'covid-19-vaccine-alerts-cowin'; const SUBJECT = 'New vaccination slots are available on COWIN. Book appointment now'; -const BODY = 'There are now new COVID-19 vaccination slots available in your requested location(s) in the next 10 days'; +const BODY = 'There are now new COVID-19 vaccination slots available in your requested location(s) in the next 10 days.'; +const FIRST_EMAIL_SUBJECT = 'Welcome to covid-19-vaccine-alerts-cowin application'; +const FIRST_EMAIL_BODY = 'There are no new COVID-19 vaccination slots available in your requested location(s) in the next 10 days.\n' + + 'The application will periodically fetch new slots if available and send email alerts.'; -module.exports = { SERVICE_PROVIDER, RECIPIENT, SENDER, SUBJECT, BODY }; +module.exports = { SERVICE_PROVIDER, RECIPIENT, SENDER, SUBJECT, BODY, FIRST_EMAIL_SUBJECT, FIRST_EMAIL_BODY }; diff --git a/src/configs/schedulerConfig.js b/src/configs/schedulerConfig.js index c33ce90..74011a3 100644 --- a/src/configs/schedulerConfig.js +++ b/src/configs/schedulerConfig.js @@ -1,4 +1,4 @@ -const SCHEDULE = '0 */3 * * *'; +const SCHEDULE = '* * * * *'; const DATE_RANGE = 10; const DATE_FORMAT = 'DD-MM-YYYY'; diff --git a/src/services/alerts.js b/src/services/alerts.js index 1b4031f..b4d5b4c 100644 --- a/src/services/alerts.js +++ b/src/services/alerts.js @@ -10,11 +10,11 @@ let mailerTransport = mailer.createTransport({ } }); -function sendEmailAlert (htmlBody, callback) { +function sendEmailAlert (subject, htmlBody, callback) { let options = { from: String(mailConfig.SENDER + `<${appConfig.EMAIL}>`), to: mailConfig.RECIPIENT, - subject: mailConfig.SUBJECT, + subject: subject, text: mailConfig.BODY, html: htmlBody }; diff --git a/src/utilities/appointments.js b/src/utilities/appointments.js index f17627c..6dfdf0c 100644 --- a/src/utilities/appointments.js +++ b/src/utilities/appointments.js @@ -14,7 +14,7 @@ async function compareVaccinationSlots(outputArray, vaccinationSlots) { let equalCount = 0; for(let counter = 0; counter < outputArray.length; counter++) { if (JSON.stringify(outputArray[counter]) == JSON.stringify(vaccinationSlots[counter])) { - equalCount = equalCount + 1; + equalCount++; } } if (equalCount == outputArray.length) { diff --git a/src/utilities/htmlBuilder.js b/src/utilities/htmlBuilder.js index 155428e..000c4e9 100644 --- a/src/utilities/htmlBuilder.js +++ b/src/utilities/htmlBuilder.js @@ -32,7 +32,7 @@ async function prepareHtmlBody(outputArray) {
-

${mailConfig.BODY}. Login to COWIN portal to book your appointment now

+

${mailConfig.BODY} Login to COWIN portal to book your appointment now

Total number of slots: ${outputArray.length}. Find details below: \n<\p> @@ -89,4 +89,40 @@ async function prepareHtmlBody(outputArray) { return html; } -module.exports = { prepareHtmlBody }; +async function prepareFirstEmail() { + return ` + + + + +
+

${mailConfig.FIRST_EMAIL_BODY}<\p> +

+ \n +


+
+
+ + COWIN platform + +
+
+ + MOHFW India + +
+
\n`; +} + +module.exports = { prepareHtmlBody, prepareFirstEmail }; From 07adc4cfd7f38eb4a1e4a0b5e6b0dd562ef3016c Mon Sep 17 00:00:00 2001 From: Abhinav Sinha Date: Tue, 11 May 2021 15:27:50 +0530 Subject: [PATCH 2/2] logger + html builder + daily digest + error handling --- README.md | 2 +- package.json | 4 +- src/api/routes.js | 8 +- src/app.js | 101 ++++++----- src/configs/apiConfig.js | 1 + src/configs/appConfig.js | 2 +- src/configs/mailConfig.js | 11 +- src/configs/schedulerConfig.js | 4 +- src/{services => utilities}/alerts.js | 2 +- src/utilities/appointments.js | 8 +- src/utilities/dailyDigest.js | 28 +++ src/utilities/htmlBuilder.js | 250 ++++++++++++++------------ src/utilities/locations.js | 2 +- src/utilities/logger.js | 9 + 14 files changed, 254 insertions(+), 178 deletions(-) rename src/{services => utilities}/alerts.js (96%) create mode 100644 src/utilities/dailyDigest.js create mode 100644 src/utilities/logger.js diff --git a/README.md b/README.md index 14a5791..114b441 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ const SERVICE_PROVIDER = 'Gmail'; const RECIPIENT = 'mail1@gmail.com,mail2@gmail.com,mail3@gmail.com'; ``` -Finally, you can also alter the date range with which the application will fetch vaccination slots by customising **DATE_RANGE** value in [`src/configs/schedulerConfig.js`](https://github.com/sinhadotabhinav/covid-19-vaccine-alerts-cowin/blob/master/src/configs/schedulerConfig.js) file. By default it is set to **10** but, you can change it to 7 or 15 for example, based on your need. The config file also allows changes in the periodic schedule with which the application runs. By default, **SCHEDULE** value depicts a cron schedule **every 3 hours at minute 0**. To alter this schedule, you need to be familiar with the [cron scheduler](https://linuxhint.com/cron_jobs_complete_beginners_tutorial/#:~:text=The%20scheduled%20commands%20and%20scripts,Task%20Scheduler%20in%20Windows%20OS). I use [Crontab Guru](https://crontab.guru) website to test my cron schedules. +Finally, you can also alter the date range with which the application will fetch vaccination slots by customising **DATE_RANGE** value in [`src/configs/schedulerConfig.js`](https://github.com/sinhadotabhinav/covid-19-vaccine-alerts-cowin/blob/master/src/configs/schedulerConfig.js) file. By default it is set to **7** but, you can change it to 10 or 15 for example, based on your need. The config file also allows changes in the periodic schedule with which the application runs. By default, **SCHEDULE** value depicts a cron schedule **every 2 hours at minute 0**. To alter this schedule, you need to be familiar with the [cron scheduler](https://linuxhint.com/cron_jobs_complete_beginners_tutorial/#:~:text=The%20scheduled%20commands%20and%20scripts,Task%20Scheduler%20in%20Windows%20OS). I use [Crontab Guru](https://crontab.guru) website to test my cron schedules. ``` const SCHEDULE = '0 */3 * * *'; diff --git a/package.json b/package.json index 183296c..8ef4794 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "covid-19-vaccine-alerts-cowin", - "version": "1.0.0", + "version": "1.0.1", "description": "This is an alerting application that sends email notifications to beneficiaries in India using COWIN platform for vaccine availability", "main": "src/app.js", "scripts": { @@ -10,7 +10,7 @@ "license": "Self", "dependencies": { "axios": "^0.21.1", - "cron": "^1.8.2", + "cron-parser": "^3.5.0", "dotenv": "^8.2.0", "moment": "^2.29.1", "node-cron": "^3.0.0", diff --git a/src/api/routes.js b/src/api/routes.js index 3eab940..fa83bea 100644 --- a/src/api/routes.js +++ b/src/api/routes.js @@ -12,7 +12,7 @@ async function getStates() { if (result.status == apiConfig.STATUS_OK) { return result; } else { - throw 'Unable to get list of states, status code ' + result.status + ': ' + result.statusText; + throw 'Unable to get list of states, status code', result.status, ':', result.statusText; } }) } @@ -28,7 +28,7 @@ async function getDistricts(districtId) { if (result.status == apiConfig.STATUS_OK) { return result; } else { - throw 'Unable to get list of districts, status code ' + result.status + ': ' + result.statusText; + throw 'Unable to get list of districts, status code', result.status, ':', result.statusText; } }) } @@ -44,7 +44,7 @@ async function getVaccinationSlotsByPincode(pinCode, date) { if (result.status == apiConfig.STATUS_OK) { return result; } else { - throw 'Unable to get vaccine sessions by pincode, status code ' + result.status + ': ' + result.statusText; + throw 'Unable to get vaccine sessions by pincode, status code', result.status, ':', result.statusText; } }) } @@ -60,7 +60,7 @@ async function getVaccinationSlotsByDistrict(districtId, date) { if (result.status == apiConfig.STATUS_OK) { return result; } else { - throw 'Unable to get vaccine sessions by distict, status code ' + result.status + ': ' + result.statusText; + throw 'Unable to get vaccine sessions by distict, status code', result.status, ':', result.statusText; } }) } diff --git a/src/app.js b/src/app.js index e8839ca..171759f 100644 --- a/src/app.js +++ b/src/app.js @@ -1,28 +1,29 @@ require('dotenv').config() -const moment = require('moment'); const cron = require('node-cron'); -const axios = require('axios'); +const moment = require('moment'); +const parser = require('cron-parser'); +const routes = require('./api/routes') const appConfig = require('./configs/appConfig'); const mailConfig = require('./configs/mailConfig'); -const schedulerConfig = require('./configs/schedulerConfig'); -const alerts = require('./services/alerts'); -const routes = require('./api/routes'); -const locations = require('./utilities/locations'); +const schedulerConfig = require('./configs/schedulerConfig');; +const alerts = require('./utilities/alerts'); const appointments = require('./utilities/appointments'); +const dailyDigest = require('./utilities/dailyDigest'); const htmlBuilder = require('./utilities/htmlBuilder'); +const locations = require('./utilities/locations'); +const logger = require('./utilities/logger'); let runCounter = 0; +let firstRun = true; let vaccinationSlots = []; async function main() { try { - await fetchVaccinationSlots(); - // cron.schedule(schedulerConfig.SCHEDULE, async () => { - // await fetchVaccinationSlots(); - // }); - } catch (e) { - console.log('There was an error fetching vaccine slots: ' + JSON.stringify(e, null, 2)); - throw e; + cron.schedule(schedulerConfig.SCHEDULE, async function() { + await fetchVaccinationSlots(); + }); + } catch (error) { + console.log(logger.getLog('There was an error fetching vaccine slots: ' + JSON.stringify(error, null, 2))); } } @@ -35,7 +36,7 @@ async function fetchVaccinationSlots() { let pincodeArray = appConfig.PINCODE.split(','); for( counter = 0; counter < pincodeArray.length; counter++) { if (pincodeArray[counter].toString().length < 6) { - console.log('Invalid pincode ' + pincodeArray[counter] + ' provided in config file: (src/config/appConfig.js).'); + console.log(logger.getLog('Invalid pincode ' + pincodeArray[counter] + ' provided in config file: (src/config/appConfig.js).')); pincodeArray.splice(counter, 1); } } @@ -52,9 +53,20 @@ async function fetchVaccinationSlots() { } getAppointmentsByDistrict(dates); } - } catch (e) { - console.log(e); + } catch (error) { + console.log(logger.getLog(error)); + } +} + +async function getTwoWeekDateArray() { + let dateArray = []; + let currentDate = moment(); + for(let counter = 1; counter <= schedulerConfig.DATE_RANGE; counter++) { + let date = currentDate.format(schedulerConfig.DATE_FORMAT); + dateArray.push(date); + currentDate.add(1, 'day'); } + return dateArray; } async function getAppointmentsByPincode(pincodeArray, dates) { @@ -66,7 +78,8 @@ async function getAppointmentsByPincode(pincodeArray, dates) { return appointments.getFilteredSlots(date, result.data.sessions); }) .catch(function (error) { - console.log('Unable to get appointment slots at pincode: ' + pin + ' for the date: ' + date + ', ' + error.response.statusText); + console.log(logger.getLog('Unable to get appointment slots at pincode: ' + pin + ' for the date: ' + date + + ', ' + error.response.statusText)); }); slotsArray.push(slots); }; @@ -90,27 +103,17 @@ async function getAppointmentsByDistrict(dates) { return appointments.getFilteredSlots(date, result.data.sessions); }) .catch(function (error) { - console.log('Unable to get appointment slots at district: ' + districtId + ' for the date: ' + date + ', ' + error.response.statusText); + console.log(logger.getLog('Unable to get appointment slots at district: ' + districtId + ' for the date: ' + date + + ', ' + error.response.statusText)); }); slotsArray.push(slots); }; sendEmailAlert(slotsArray); - } catch (e) { - console.log(e); + } catch (error) { + console.log(logger.getLog(error)); } } -async function getTwoWeekDateArray() { - let dateArray = []; - let currentDate = moment(); - for(let counter = 1; counter <= schedulerConfig.DATE_RANGE; counter++) { - let date = currentDate.format(schedulerConfig.DATE_FORMAT); - dateArray.push(date); - currentDate.add(1, 'day'); - } - return dateArray; -} - async function sendEmailAlert(slotsArray) { let outputArray = []; for(let counter1 = 0; counter1 < slotsArray.length; counter1++) { @@ -122,28 +125,40 @@ async function sendEmailAlert(slotsArray) { } if (outputArray.length > 0 && !Boolean(await appointments.compareVaccinationSlots(outputArray, vaccinationSlots))) { vaccinationSlots = outputArray; - alerts.sendEmailAlert(mailConfig.SUBJECT, await htmlBuilder.prepareHtmlBody(outputArray), (err, result) => { - if(err) { - console.error({err}); + alerts.sendEmailAlert(mailConfig.SUBJECT, await htmlBuilder.prepareHtmlBody(outputArray), (error, result) => { + if(error) { + console.log(logger.getLog(error)); } else { - console.log('New sessions have been processed and sent as an email alert to recipient(s).'); + console.log(logger.getLog('New sessions have been processed and sent as an email alert to the recipient(s).')); } }); } else { - console.log('No new sessions to process at this time.'); - if (runCounter == 0) { - alerts.sendEmailAlert(mailConfig.FIRST_EMAIL_SUBJECT, await htmlBuilder.prepareFirstEmail(), (err, result) => { - if(err) { - console.error({err}); + console.log(logger.getLog('No new sessions to process at this time.')); + if (Boolean(firstRun)) { + alerts.sendEmailAlert(mailConfig.FIRST_EMAIL_SUBJECT, await htmlBuilder.prepareFirstEmail(), (error, result) => { + if(error) { + console.log(logger.getLog(error)); } else { - console.log('Welcome email alert has been sent to recipient(s).'); + console.log(logger.getLog('Welcome email alert has been sent to the recipient(s).')); } }); } } - runCounter++; + firstRun = false; + runCounter = runCounter + 1; + await dailyDigest.updateRunCounter(runCounter, vaccinationSlots); + await resetDailyCounter(); +}; + +async function resetDailyCounter() { + const interval = parser.parseExpression(schedulerConfig.SCHEDULE); + let difference = moment(new Date(interval.next().toString())).diff(moment(), 'days', true); + if (difference.toFixed() > 0) { + await dailyDigest.prepareReport(); + runCounter = 0; + } }; main().then(() => { - console.log('The covid-19 vaccine alerting application has started!'); + console.log(logger.getLog('The covid-19 vaccination alerting application has started!')); }); diff --git a/src/configs/apiConfig.js b/src/configs/apiConfig.js index e677e81..21dec96 100644 --- a/src/configs/apiConfig.js +++ b/src/configs/apiConfig.js @@ -10,6 +10,7 @@ const STATES_URI = '/admin/location/states'; const DISTRICTS_URI = '/admin/location/districts/'; const APPOINTMENTS_PINCODE_URI = '/appointment/sessions/public/findByPin'; const APPOINTMENTS_DISTRICTS_URI = '/appointment/sessions/public/findByDistrict'; + let xApiKey = ''; if (typeof Buffer.from === "function") { xApiKey = Buffer.from(X_API_KEY, 'base64'); diff --git a/src/configs/appConfig.js b/src/configs/appConfig.js index c897808..68cf9a1 100644 --- a/src/configs/appConfig.js +++ b/src/configs/appConfig.js @@ -1,4 +1,4 @@ -const FINDBYPINCODE = true; +const FINDBYPINCODE = false; // location configs const PINCODE = '800001'; const STATE = 'BIHAR'; diff --git a/src/configs/mailConfig.js b/src/configs/mailConfig.js index b0e5eea..80f4d30 100644 --- a/src/configs/mailConfig.js +++ b/src/configs/mailConfig.js @@ -2,9 +2,12 @@ const SERVICE_PROVIDER = 'Gmail'; const RECIPIENT = 'asinha093@gmail.com'; const SENDER = 'covid-19-vaccine-alerts-cowin'; const SUBJECT = 'New vaccination slots are available on COWIN. Book appointment now'; -const BODY = 'There are now new COVID-19 vaccination slots available in your requested location(s) in the next 10 days.'; +const DAILY_DIGEST_SUBJECT = 'Daily digest: covid-19-vaccine-alerts-cowin'; const FIRST_EMAIL_SUBJECT = 'Welcome to covid-19-vaccine-alerts-cowin application'; -const FIRST_EMAIL_BODY = 'There are no new COVID-19 vaccination slots available in your requested location(s) in the next 10 days.\n' + - 'The application will periodically fetch new slots if available and send email alerts.'; +const COWIN_URL = 'https://www.cowin.gov.in/home'; +const MOFHW_URL = 'https://www.mohfw.gov.in/covid_vaccination/vaccination/faqs.html'; +const COWIN_LOGO_URL = 'https://github.com/sinhadotabhinav/covid-19-vaccine-alerts-cowin/blob/master/src/assets/cowin-logo.png?raw=true'; +const WHO_VACCINE_LOGO_URL = 'https://github.com/sinhadotabhinav/covid-19-vaccine-alerts-cowin/blob/master/src/assets/vaccine.png?raw=true'; -module.exports = { SERVICE_PROVIDER, RECIPIENT, SENDER, SUBJECT, BODY, FIRST_EMAIL_SUBJECT, FIRST_EMAIL_BODY }; +module.exports = { SERVICE_PROVIDER, RECIPIENT, SENDER, SUBJECT, DAILY_DIGEST_SUBJECT, FIRST_EMAIL_SUBJECT, COWIN_URL, MOFHW_URL, + COWIN_LOGO_URL, WHO_VACCINE_LOGO_URL }; diff --git a/src/configs/schedulerConfig.js b/src/configs/schedulerConfig.js index 74011a3..726a92a 100644 --- a/src/configs/schedulerConfig.js +++ b/src/configs/schedulerConfig.js @@ -1,5 +1,5 @@ -const SCHEDULE = '* * * * *'; -const DATE_RANGE = 10; +const SCHEDULE = '0 */2 * * *'; +const DATE_RANGE = 7; const DATE_FORMAT = 'DD-MM-YYYY'; module.exports = { SCHEDULE, DATE_RANGE, DATE_FORMAT }; diff --git a/src/services/alerts.js b/src/utilities/alerts.js similarity index 96% rename from src/services/alerts.js rename to src/utilities/alerts.js index b4d5b4c..9c4cfad 100644 --- a/src/services/alerts.js +++ b/src/utilities/alerts.js @@ -15,7 +15,7 @@ function sendEmailAlert (subject, htmlBody, callback) { from: String(mailConfig.SENDER + `<${appConfig.EMAIL}>`), to: mailConfig.RECIPIENT, subject: subject, - text: mailConfig.BODY, + text: 'Email alert', html: htmlBody }; mailerTransport.sendMail(options, (error, info) => { diff --git a/src/utilities/appointments.js b/src/utilities/appointments.js index 6dfdf0c..46611c0 100644 --- a/src/utilities/appointments.js +++ b/src/utilities/appointments.js @@ -1,10 +1,10 @@ const appConfig = require('../configs/appConfig'); +const logger = require('../utilities/logger'); function getFilteredSlots (date, sessions) { let validSlots = sessions.filter(slot => slot.min_age_limit <= appConfig.AGE && slot.available_capacity > 0) if(validSlots.length > 0) { - console.log('Vaccination slots returned:'); - console.log({date:date, validSlots: validSlots.length}); + console.log(logger.getLog(`Vaccination slots returned for ${date}: ${validSlots.length}`)); } return validSlots; }; @@ -13,8 +13,8 @@ async function compareVaccinationSlots(outputArray, vaccinationSlots) { if (outputArray.length == vaccinationSlots.length) { let equalCount = 0; for(let counter = 0; counter < outputArray.length; counter++) { - if (JSON.stringify(outputArray[counter]) == JSON.stringify(vaccinationSlots[counter])) { - equalCount++; + if (outputArray[counter].name == vaccinationSlots[counter].name) { + equalCount = equalCount + 1; } } if (equalCount == outputArray.length) { diff --git a/src/utilities/dailyDigest.js b/src/utilities/dailyDigest.js new file mode 100644 index 0000000..f512233 --- /dev/null +++ b/src/utilities/dailyDigest.js @@ -0,0 +1,28 @@ +const appConfig = require('../configs/appConfig'); +const mailConfig = require('../configs/mailConfig'); +const alerts = require('../utilities/alerts'); +const htmlBuilder = require('../utilities/htmlBuilder'); +const logger = require('../utilities/logger'); + +let runs = 0; +let slots = []; + +async function prepareReport () { + let regionType = Boolean(appConfig.FINDBYPINCODE) ? 'pincode' : 'district'; + let region = regionType == 'pincode' ? appConfig.PINCODE : appConfig.DISTRICT; + alerts.sendEmailAlert(mailConfig.DAILY_DIGEST_SUBJECT, + await htmlBuilder.prepareDailyDigestEmail(runs, slots.length, regionType, region), (error, result) => { + if(error) { + console.log(logger.getLog(error)); + } else { + console.log(logger.getLog('Daily digest email alert has been sent to the recipient(s).')); + } + }); +} + +async function updateRunCounter (runCounter, vaccinationSlots) { + runs = runCounter; + slots = vaccinationSlots; +} + +module.exports = { prepareReport, updateRunCounter }; diff --git a/src/utilities/htmlBuilder.js b/src/utilities/htmlBuilder.js index 000c4e9..5f94952 100644 --- a/src/utilities/htmlBuilder.js +++ b/src/utilities/htmlBuilder.js @@ -1,128 +1,148 @@ const mailConfig = require('../configs/mailConfig'); +const schedulerConfig = require('../configs/schedulerConfig'); async function prepareHtmlBody(outputArray) { let html = ` - - - - -
-

${mailConfig.BODY} Login to COWIN portal to book your appointment now

-

Total number of slots: ${outputArray.length}. Find details below: \n<\p> -

- - - - - - - - - - - - - \n`; - for (let counter = 0; counter < outputArray.length; counter++) { - let slots = outputArray[counter].slots.toString().replace(/,/g, '; '); - html = html + - ` - - - - - - - - - - - - \n`; - } + + + + +
+

There are now new COVID-19 vaccination slots available in your requested location(s) in the next ${schedulerConfig.DATE_RANGE} days. + Login to COWIN portal to book your appointment now

+

Total number of centers with slots available are: ${outputArray.length}. Find details below: \n<\p> +

#DateDistrictCenter NameCenter AddressPincodeVaccine NameAge LimitAvailabilityAvailable SlotsFee Type
${counter + 1}${outputArray[counter].date}${outputArray[counter].district_name}${outputArray[counter].name}${outputArray[counter].address}${outputArray[counter].pincode}${outputArray[counter].vaccine}${outputArray[counter].min_age_limit}${outputArray[counter].available_capacity}${slots}${outputArray[counter].fee_type}
+ + + + + + + + + + + + + \n`; + for (let counter = 0; counter < outputArray.length; counter++) { + let slots = outputArray[counter].slots.toString().replace(/,/g, '; '); html = html + - `
#DateDistrictCenter NameCenter AddressPincodeVaccine NameAge LimitAvailabilityAvailable SlotsFee Type
-

- \n -


-
-
- - COWIN platform - -
-
- - MOHFW India - + ` + ${counter + 1} + ${outputArray[counter].date} + ${outputArray[counter].district_name} + ${outputArray[counter].name} + ${outputArray[counter].address} + ${outputArray[counter].pincode} + ${outputArray[counter].vaccine} + ${outputArray[counter].min_age_limit} + ${outputArray[counter].available_capacity} + ${slots} + ${outputArray[counter].fee_type} + \n`; + } + html = html + ` +
-
\n`; + \n +

+ ${getHtmlFooter()}\n + `; return html; } async function prepareFirstEmail() { return ` - - - - -
-

${mailConfig.FIRST_EMAIL_BODY}<\p> -

- \n -


-
-
- - COWIN platform - -
-
- - MOHFW India - -
-
\n`; + + + + +
+

There are no new COVID-19 vaccination slots available in your requested location(s) in the next ${schedulerConfig.DATE_RANGE} days.\n + The application will periodically fetch new slots if available and send email alerts.<\p> +

+ \n +

+ ${getHtmlFooter()}\n + `; +} + +async function prepareDailyDigestEmail(runs, centers, regionType, region) { + return ` + + + + +
+

Welcome to the ${mailConfig.SENDER} daily digest. A summary of results obtained today are gathered and presented below:<\p> +

The application periodically checked for new vaccination slots ${runs} times today.<\p> +

Total number of vaccination centers with available slots found for ${regionType} \(${region}\) were: ${centers}<\p> +

The application will continue to look for new slots in your requested region and send alerts tomorrow.<\p> +

+ \n +

+ ${getHtmlFooter()}\n + `; +} + +function getHtmlFooter() { + return ` +
+
+ + COWIN platform + +
+
+ + MOHFW India + +
+
`; +} + +function getImageStyles() { + return ` + img { + border: 1px solid #ddd; + border-radius: 4px; + padding: 5px; + width: 150px; + }`; +} + +function getTableStyles() { + return ` + table { + font-family: arial, sans-serif; + border-collapse: collapse; + width: 100%; + } + th { + background-color: #f1f1f1; + font-size: 9pt; + border: 1px solid #dddddd; + text-align: left; + padding: 8px; + } + td { + border: 1px solid #dddddd; + text-align: left; + font-size: 8pt; + padding: 8px; + }`; } -module.exports = { prepareHtmlBody, prepareFirstEmail }; +module.exports = { prepareHtmlBody, prepareFirstEmail, prepareDailyDigestEmail }; diff --git a/src/utilities/locations.js b/src/utilities/locations.js index d2fd504..3536ed3 100644 --- a/src/utilities/locations.js +++ b/src/utilities/locations.js @@ -1,5 +1,5 @@ -var stateJson = require('../models/states.json'); const routes = require('../api/routes'); +var stateJson = require('../models/states.json'); function getStateId (state) { for(var counter = 0; counter < stateJson['states'].length; counter++) { diff --git a/src/utilities/logger.js b/src/utilities/logger.js new file mode 100644 index 0000000..cb237bb --- /dev/null +++ b/src/utilities/logger.js @@ -0,0 +1,9 @@ +const moment = require('moment'); +const schedulerConfig = require('../configs/schedulerConfig'); + +function getLog(event) { + let timestamp = moment().format(schedulerConfig.DATE_FORMAT + ' HH:mm:ss').toString(); + return `${timestamp}, ${event}`; +}; + +module.exports = { getLog };