diff --git a/docker-compose.yaml b/docker-compose.yaml index 6194599..ba0c8b1 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -22,6 +22,9 @@ services: SLACK_ALERT_CHANNEL_ID: ${SLACK_ALERT_CHANNEL_ID} SLACK_CHANNEL_ID: ${SLACK_CHANNEL_ID} SLACK_XOXB: ${SLACK_XOXB} + TWILIO_ACCOUNT_SID: ${TWILIO_ACCOUNT_SID} + TWILIO_AUTH_TOKEN: ${TWILIO_AUTH_TOKEN} + TWILIO_PHONE_NUMBER: ${TWILIO_PHONE_NUMBER} VOLUNTEER_DISPATCH_PREVENT_PROCESSING: ${VOLUNTEER_DISPATCH_PREVENT_PROCESSING} VOLUNTEER_DISPATCH_STATE: ${VOLUNTEER_DISPATCH_STATE} volumes: diff --git a/package-lock.json b/package-lock.json index e2eb986..0e7fd75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -881,12 +881,50 @@ "@babel/types": "^7.3.0" } }, + "@types/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", "dev": true }, + "@types/connect": { + "version": "3.4.33", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz", + "integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==", + "requires": { + "@types/node": "*" + } + }, + "@types/express": { + "version": "4.17.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.6.tgz", + "integrity": "sha512-n/mr9tZI83kd4azlPG5y997C/M4DNABK9yErhFM6hKdym4kkmd9j0vtsJyjFIwfRBxtrxZtAfGZCNRIBMFLK5w==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.8.tgz", + "integrity": "sha512-1SJZ+R3Q/7mLkOD9ewCBDYD2k0WyZQtWYqF/2VvoNN2/uhI49J9CDN4OAm+wGMA0DbArA4ef27xl4+JwMtGggw==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, "@types/istanbul-lib-coverage": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", @@ -918,12 +956,41 @@ "integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==", "dev": true }, + "@types/mime": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.2.tgz", + "integrity": "sha512-4kPlzbljFcsttWEq6aBW0OZe6BDajAmyvr2xknBG92tejQnvdGtT9+kXSZ580DqpxY9qG2xeQVF9Dq0ymUTo5Q==" + }, + "@types/node": { + "version": "14.0.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.14.tgz", + "integrity": "sha512-syUgf67ZQpaJj01/tRTknkMNoBBLWJOBODF0Zm4NrXmiSuxjymFrxnTu1QVYRubhVkRcZLYZG8STTwJRdVm/WQ==" + }, "@types/prettier": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-1.19.1.tgz", "integrity": "sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ==", "dev": true }, + "@types/qs": { + "version": "6.9.3", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.3.tgz", + "integrity": "sha512-7s9EQWupR1fTc2pSMtXRQ9w9gLOcrJn+h7HOXw4evxyvVqMi4f+q7d2tnFe3ng3SNHjtK+0EzGMGFUQX4/AQRA==" + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" + }, + "@types/serve-static": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.4.tgz", + "integrity": "sha512-jTDt0o/YbpNwZbQmE/+2e+lfjJEJJR0I3OFaKQKPWkASkCoW3i6fsUnqudSMcNAfbtmADGu8f4MV4q+GqULmug==", + "requires": { + "@types/express-serve-static-core": "*", + "@types/mime": "*" + } + }, "@types/stack-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", @@ -1261,6 +1328,11 @@ "es-abstract": "^1.17.0-next.1" } }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + }, "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", @@ -1639,6 +1711,11 @@ "node-int64": "^0.4.0" } }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -2076,6 +2153,11 @@ "whatwg-url": "^7.0.0" } }, + "dayjs": { + "version": "1.8.28", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.28.tgz", + "integrity": "sha512-ccnYgKC0/hPSGXxj7Ju6AV/BP4HUkXC2u15mikXT5mX9YorEaoi1bEKOmAqdkJHN4EEkmAf97SpH66Try5Mbeg==" + }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -2264,6 +2346,14 @@ "safer-buffer": "^2.1.0" } }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -5704,6 +5794,30 @@ "minimist": "^1.2.5" } }, + "jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -5715,6 +5829,25 @@ "verror": "1.10.0" } }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -5787,6 +5920,41 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -6502,6 +6670,11 @@ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, + "phone": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/phone/-/phone-2.4.12.tgz", + "integrity": "sha512-OZwRsDCP6Oq/bxxXCzkOTgz/q5YUjPI4KT5QWazfaf+/aIOGgHq7sh34Wk8nPsW1zze6rZEwfnn4GxUMVv3t4w==" + }, "picomatch": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", @@ -6538,6 +6711,11 @@ "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", "dev": true }, + "pop-iterate": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pop-iterate/-/pop-iterate-1.0.1.tgz", + "integrity": "sha1-zqz9q0q/NT16DyqqLB/Hs/lBO6M=" + }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -6686,11 +6864,26 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, + "q": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/q/-/q-2.0.3.tgz", + "integrity": "sha1-dbjbAlWhpa+C9Yw/Oqoe/sfQ0TQ=", + "requires": { + "asap": "^2.0.0", + "pop-iterate": "^1.0.1", + "weak-map": "^1.0.5" + } + }, "qs": { "version": "6.9.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz", "integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==" }, + "querystringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz", + "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==" + }, "random-js": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/random-js/-/random-js-2.1.0.tgz", @@ -6919,6 +7112,11 @@ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "dev": true }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" + }, "resolve": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", @@ -6991,6 +7189,11 @@ "glob": "^7.1.3" } }, + "rootpath": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/rootpath/-/rootpath-0.1.2.tgz", + "integrity": "sha1-Wzeah9ypBum5HWkKWZQ5vvJn6ms=" + }, "rsvp": { "version": "4.8.5", "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", @@ -7215,6 +7418,11 @@ "xmlchars": "^2.1.1" } }, + "scmp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/scmp/-/scmp-2.1.0.tgz", + "integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==" + }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -8068,6 +8276,24 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, + "twilio": { + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/twilio/-/twilio-3.47.0.tgz", + "integrity": "sha512-c/apwtmc1Gwvn0EhjGaWaeSuQUKkGykUFJeVoIKWsXYVamAS+4WKC95H7yGNCGDHtB9x5+lxC1lkjyFzy260pg==", + "requires": { + "@types/express": "^4.17.3", + "axios": "^0.19.2", + "dayjs": "^1.8.21", + "jsonwebtoken": "^8.5.1", + "lodash": "^4.17.15", + "q": "2.0.x", + "qs": "^6.9.1", + "rootpath": "^0.1.2", + "scmp": "^2.1.0", + "url-parse": "^1.4.7", + "xmlbuilder": "^13.0.2" + } + }, "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", @@ -8237,6 +8463,15 @@ "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", "dev": true }, + "url-parse": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz", + "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "url-parse-lax": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", @@ -8346,6 +8581,11 @@ "makeerror": "1.0.x" } }, + "weak-map": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/weak-map/-/weak-map-1.0.5.tgz", + "integrity": "sha1-eWkVhNmGB/UHC9O3CkDmuyLkAes=" + }, "webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", @@ -8597,6 +8837,11 @@ "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", "dev": true }, + "xmlbuilder": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz", + "integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==" + }, "xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", diff --git a/package.json b/package.json index bb65474..4e3a2be 100644 --- a/package.json +++ b/package.json @@ -31,10 +31,12 @@ "google-libphonenumber": "^3.2.8", "moment": "^2.26.0", "node-geocoder": "^3.25.1", + "phone": "^2.4.12", "preconditions": "^2.2.3", "qs": "^6.9.3", "random-js": "^2.1.0", "slack": "^11.0.2", + "twilio": "^3.47.0", "winston": "^3.2.1", "winston-transport": "^4.3.0" }, diff --git a/src/config.js b/src/config.js index 81258d4..7696946 100644 --- a/src/config.js +++ b/src/config.js @@ -51,6 +51,11 @@ const config = { SLACK_SIGNING_SECRET: process.env.SLACK_SIGNING_SECRET, SLACK_CHANNEL_ID: process.env.SLACK_CHANNEL_ID, SLACK_ALERT_CHANNEL_ID: process.env.SLACK_ALERT_CHANNEL_ID, + + // Twilio + TWILIO_ACCOUNT_SID: process.env.TWILIO_ACCOUNT_SID, + TWILIO_AUTH_TOKEN: process.env.TWILIO_AUTH_TOKEN, + TWILIO_PHONE_NUMBER: process.env.TWILIO_PHONE_NUMBER, }; module.exports = Object.freeze(config); diff --git a/src/index.js b/src/index.js index bb86343..54b60a1 100644 --- a/src/index.js +++ b/src/index.js @@ -11,6 +11,7 @@ const Request = require("./model/request-record"); const RequesterService = require("./service/requester-service"); const RequestService = require("./service/request-service"); const VolunteerService = require("./service/volunteer-service"); +const { formatPhoneNumber } = require("./utils/phone-number-utils"); const { sendDispatch } = require("./slack/sendDispatch"); require("dotenv").config(); @@ -294,6 +295,92 @@ async function checkForNewSubmissions() { nextPage(); }); + + // Check if Twilio credentials are present, in order to send followup text + const accountSid = config.TWILIO_ACCOUNT_SID; + const authToken = config.TWILIO_AUTH_TOKEN; + const client = require("twilio")(accountSid, authToken); + const twilioPhoneNumber = config.TWILIO_PHONE_NUMBER; + const hasTwilioCredentials = + (accountSid && authToken && twilioPhoneNumber) || false; + + // Check Airtable for tasks completed in the last day, then send volunteer + // a followup text + base(config.AIRTABLE_REQUESTS_TABLE_NAME) + .select({ + view: config.AIRTABLE_REQUESTS_VIEW_NAME, + filterByFormula: ` + AND( + {Status} = 'Completed', + IS_AFTER({Last modified time}, (DATEADD(TODAY(), -1, 'days'))), + {Followup SMS Sent?} = 'No' + )`, + }) + .eachPage(async (records, nextPage) => { + if (!records.length) return; + + for (const record of records) { + const volunteerId = record.get("Assigned Volunteer")[0]; + + base(config.AIRTABLE_VOLUNTEERS_TABLE_NAME).find(volunteerId, function ( + err, + rec + ) { + if (err) { + logger.error(err); + return; + } + const phoneNumber = rec.get( + "Please provide your contact phone number:" + ); + const formattedPhoneNumber = formatPhoneNumber(phoneNumber); + + if (hasTwilioCredentials) { + logger.info(`Sending followup text to: ${formattedPhoneNumber}`); + client.messages + .create({ + body: "Thank you for being a great neighbor!", + from: config.TWILIO_PHONE_NUMBER, + to: formattedPhoneNumber, + }) + .then((message) => { + logger.info(`Message SID: ${message.sid}`); + base(config.AIRTABLE_REQUESTS_TABLE_NAME).update( + [ + { + id: record.id, + fields: { + "Followup SMS Sent?": "Yes", + }, + }, + ], + function (err, records) { + if (err) { + logger.error(err); + return; + } + records.forEach(function (record) { + logger.info( + `Followup text sent?: ${record.get( + "Followup SMS Sent?" + )}` + ); + }); + } + ); + }) + .catch((error) => { + logger.error(`onRejected function called: ${error.message}`); + }); + } else { + logger.error( + "Twilio credentials missing -- Followup text not sent" + ); + } + }); + } + nextPage(); + }); } /** diff --git a/src/utils/phone-number-utils.js b/src/utils/phone-number-utils.js index fcf0683..e7da266 100644 --- a/src/utils/phone-number-utils.js +++ b/src/utils/phone-number-utils.js @@ -1,5 +1,6 @@ const PNF = require("google-libphonenumber").PhoneNumberFormat; const phoneUtil = require("google-libphonenumber").PhoneNumberUtil.getInstance(); +const phone = require("phone"); /** * Parse phone numbers @@ -40,6 +41,17 @@ const getDisplayNumber = (rawInput) => { return displayNumber; }; +/** + * Normalizes mobile phone number into E.164 format + * + * @param {string} phoneNumber - Mobile phone number + * @returns {string} E.164-formatted mobile phone number + */ +const formatPhoneNumber = (phoneNumber) => { + return phone(phoneNumber)[0]; +}; + module.exports = { getDisplayNumber, + formatPhoneNumber, }; diff --git a/test/utils/phone-number-utils.test.js b/test/utils/phone-number-utils.test.js index 171b680..5ad907a 100644 --- a/test/utils/phone-number-utils.test.js +++ b/test/utils/phone-number-utils.test.js @@ -1,4 +1,7 @@ -const { getDisplayNumber } = require("../../src/utils/phone-number-utils"); +const { + getDisplayNumber, + formatPhoneNumber, +} = require("../../src/utils/phone-number-utils"); test("Return kebab-case number when passed number is formatted with parens", () => { expect(getDisplayNumber("(212) 222-2222")).toBe("212-222-2222"); @@ -8,7 +11,7 @@ test("Return parsed number when 10 unformatted digits are passed", () => { expect(getDisplayNumber("2122222222")).toBe("212-222-2222"); }); -test("Return parsed number when 11 unformattted digits are passed", () => { +test("Return parsed number when 11 unformatted digits are passed", () => { expect(getDisplayNumber("12122222222")).toBe("212-222-2222"); }); @@ -22,3 +25,23 @@ test("Return raw input (plus flag string) if unparseable value is passed", () => `${unparseableValue} _[Bot note: unparseable number.]_` ); }); + +test("Return E.164-formatted number when passed number is formatted with parens", () => { + expect(formatPhoneNumber("(212) 222-2222")).toBe("+12122222222"); +}); + +test("Return E.164-formatted number when 10 unformatted digits are passed", () => { + expect(formatPhoneNumber("2122222222")).toBe("+12122222222"); +}); + +test("Return E.164-formatted number when 11 unformatted digits are passed", () => { + expect(formatPhoneNumber("12122222222")).toBe("+12122222222"); +}); + +test("Return undefined if no value is passed", () => { + expect(formatPhoneNumber()).toBeUndefined(); +}); + +test("Return undefined if less than 10 digits are passed", () => { + expect(formatPhoneNumber("212-222-222")).toBeUndefined(); +});