UNPKG

poserver

Version:
1,166 lines (1,006 loc) 48.2 kB
/** * Created by tomdaley on 9/18/16. */ "use strict"; /** * P R I V A T E U T I L I T I E S */ /** * @namespace * @property {object} err - Error thrown in code * @property {object} err.stack - Stack trace */ var params = require("../../poserver-configuration.json"); var prompts = require('./const/prompts.js'); var mongoUtil = require('./classes/mongoUtils'); var Person = require('./classes/clsPerson'); var ParsedAddress = require('./classes/clsParsedAddress'); var ParsedName = require('./classes/clsParsedName'); var ObjectId = require('mongodb').ObjectId; var https = require('https'); var bot; var builder = require('botbuilder'); var violentVerbs = [{}]; exports.violentVerbs = violentVerbs; module.exports = { setBot: function (abot) { bot = abot; }, /** * Try to get short, unique names for each of two people. Examples * * First Full Name Second Full Name Returns * Thomas J. Daley Donald J. Trump Thomas, Donald * Thomas J. Daley Thomas J. Trump Thomas Daley, Thomas Trump * Richard J. Daley Richard M. Daley Richard J. Daley, Richard M. Daley * John Doe John Doe the victim, the perpetrator * * @param {object} parsedName1 - Object having name-part properties, e.g. return value of ParsedName.parseName() * @param {object} parsedName2 - Object having name-part properties, e.g. return value of ParsedName.parseName() * @param {string=} default1 - Role name to assign to first person if the two names are the same (default=the perpetrator) * @param {string=} default2 - Role name to assign to second person if the two names are the same (default=the victim) * @returns {object} - .name1 and .name2 properties uniquely identify the two people */ getUniqueNames: function (parsedName1, parsedName2, default1, default2) { default1 = default1 || "the perpetrator"; default2 = default2 || "the victim"; var aName = parsedName1.firstName; var vName = parsedName2.firstName; if (aName == vName) { aName = aName + " " + parsedName1.lastName; vName = vName + " " + parsedName2.lastName; } if (aName == vName) { aName = parsedName1.fullName; vName = parsedName2.fullName; } if (aName == vName) { aName = default1; vName = default2; } //The actorName and victimName properties are for backward compatibility with a prior version of this //function. return {actorName: aName, victimName: vName, name1: aName, name2: vName}; }, /** * Try to load this user's profile from our database. Users can come to us from a number of channels, so look * for the channel-specific identity for this user. See the MongoDb users table to see how this works. * * @param session */ getUserProfile: function (session) //, callback) { return new Promise(function (resolve, reject) { //var username = session.message.user.name; var userid = session.message.user.id; var idsource = session.message.address.channelId; var query = {"channelIdentities": {$elemMatch: {"source": idsource, "id": userid}}}; var projection = { "email": 1, "name": 1, "cellPhone": 1, "telephone": 1, "address": 1, "fax": 1, "ids" : 1, "options": 1, "gender": 1, "birthDate": 1, "channelIdentities": 1, "_id" : 1 }; var collection = mongoUtil.getDb().collection('users'); collection.find(query).project(projection).limit(1).toArray(function (err, docs) { if (err) { reject(err); } else { var isLoaded = false; if (!err && docs.length > 0) { session.userData.userProfile = new Person(docs[0]); isLoaded = true; } if (!session.userData.hasOwnProperty("config")) session.userData.config = {}; resolve(isLoaded); } }); }); }, /** * Add the protective order information to our backend queue for document generation or attorney review. * * @param session */ queueProtectiveOrder: function (session) { var queuedItem = {}; queuedItem.usState = "TX"; queuedItem.form = "FAMVIOPO"; queuedItem.formData = {}; queuedItem.formData.victim = session.userData.victim; queuedItem.formData.actor = session.userData.actor; queuedItem.formData.violentActs = session.userData.violentActs; this.queueItem(session, queuedItem); }, /** * Queue an item for later processing, for example by form generation software or placing a phone call, etc. * * @param session * @param item * * @return {Promise} */ queueItem: function (session, item) { //Associate this item with the user who created it. item.users_id = new ObjectId(session.userData.userProfile.users_id); //Remember what time we queued this item. item.queueTime = (new Date()).toJSON(); item.status = "Q"; //Mark item as queued and ready for merging. if (!item.hasOwnProperty("paid")) item.paid = "N"; //Indicate that this item has not been paid for yet. //Now to actually insert the item in our processing queue, which for now is a MongoDB collection return mongoUtil.getDb().collection('queue').insertOne(item); }, /** * * @param {string} id - ObjectId (as string) of queued item to update * @param {string} flagValue - Y or N to indicate whether item is paid. Default = Y. */ setQueuedItemPaymentFlag: function (id, flagValue) { flagValue = flagValue || 'Y'; var filter = {_id: new ObjectId(id)}; var update = {$set: {paid: flagValue}}; var options = {}; //No callback or promise because I don't care when this happens. mongoUtil.getDb().collection('queue').updateOne(filter, update, options) .catch(function (reason) { console.error("JDBotUtil.setQueuedItemPaymentFlag(): Error setting payment flag."); console.error(reason); console.error(filter); console.error(update); }); }, saveOriginalAnswer: function (session) { //First save any new data we captured relating to the user's profile. this.saveUserProfile(session.userData.userProfile, session) .then(function (result) { //console.log("********** saveOriginalAnswer()"); //console.log(result); }) .catch(function (reason) { console.error("JDBotUtil.saveOriginalAnswer - Error updating user profile"); console.error(reason); }); //Create the item to queue for document generation var item = {}; item.usState = session.userData.jurisdiction.state; //"TX"; item.countryCode = session.userData.jurisdiction.country; //"US"; item.form = "ANSWER"; item.formData = session.userData.case; if (session.userData.case.hasOwnProperty("paid")) item.paid = session.userData.case.paid; //Save the case information to the cases collection var aCase = {}; aCase.state = item.usState; aCase.county = session.userData.case.county; aCase.country = item.countryCode; aCase.causeNumber = session.userData.case.causeNumber; aCase.courtNumber = session.userData.case.courtNumber; aCase.courtType = session.userData.case.courtType; aCase.matterTypeDescription = session.userData.case.matterTypeDescription; aCase.matterType = session.userData.case.matterType; aCase.petitioner = {}; var pName = new ParsedName(session.userData.case.petitioner.name); aCase.petitioner.name = pName; aCase.petitioner.fullName = pName.fullName(); aCase.respondent = {}; pName = new ParsedName(session.userData.userProfile.name); aCase.respondent.name = pName; aCase.respondent.fullName = pName.fullName(); aCase.respondent.users_id = new ObjectId(session.userData.userProfile.users_id); if (session.userData.case.hasOwnProperty("children")) aCase.children = session.userData.case.children; var query = { "state" : aCase.state, "county" : aCase.county, "causeNumber": aCase.causeNumber, "country" : aCase.country }; var options = {"upsert": true}; mongoUtil.getDb().collection('cases').updateOne(query, aCase, options) .then(function (document) { console.info("JDBotUtil.saveOriginalAnswer(): case saved"); }) .catch(function (reason) { console.error("JDBotUtil.saveOriginalAnswer(): Failed to save case data."); console.error(reason); console.error(aCase); }); //Save some current state data, if configured to do so. This is just for debugging. if (params.saveUserData) { console.info("JDBotUtil.saveOriginalAnswer(): Persisting session.userData"); var persistedData = {}; var keys = Object.keys(session.userData); var newKey; //Don't try to save the _id property, if there is one. Replace any dots in key names with dashes. //Note that I don't iterate through every key in every child object because if there are dots in keys, //I expect the originate from the BotBuilder API and would be saved at a top-level property of the //userData object. for (var k in keys) { if (keys[k] !== "_id") { newKey = keys[k].replace(/\./g, "-").replace(/["']/g, ""); persistedData[newKey] = session.userData[keys[k]]; } } mongoUtil.getDb().collection('userData').insertOne(persistedData) .then(function (result) { console.info("JDBotUtil.saveOriginalAnswer(): Successfully persisted session.userData."); }) .catch(function (reason) { console.error("JDBotUtil.saveOriginalAnswer(): Error persisting session.userData"); console.error(reason); console.error(persistedData); }); } //Now queue the form for generation and review return this.queueItem(session, item); }, /** * No callback or promise for now because I think this will complete before the user can answer the next * series of questions. * * @param {Person} user - Person object containing channelIds to be saved */ saveIds: function (user) { var filter = {_id: new ObjectId(user.users_id)}; var update = {$set: {ids: user.ids}}; var options = {}; mongoUtil.getDb().collection('users').updateOne(filter, update, options) .then(function (document) { console.info("JDBotUtil.saveIds(): Successfully saved ids"); }) .catch(function (reason) { console.error("JDBotUtil.saveIds(): Error saving ids"); console.error(reason); console.error(filter); console.error(update); }); }, /** * Used to remove an id associated with a user. For example, if we find that the user's Stripe id is no longer * valid, we would remove the "stripe" id from the ids property so we wouldn't try that id again. * * @param {string} users_id - _id of users record to update * @param {string} idName - name of users.ids property to delete */ deleteId: function (users_id, idName) { var filter = {_id: new ObjectId(users_id)}; var projection = {_id: 1, ids: 1}; var options = {}; mongoUtil.getDb().collection('users').find(filter, projection, options).toArray() .then(function (docs) { if (docs.length === 1) { var ids = docs[0].ids; delete ids[idName]; var filter = {_id: new ObjectId(users_id)}; var update = {$set: {ids: ids}}; var options = {}; mongoUtil.getDb().collection('users').updateOne(filter, update, options) .then(function (document) { // }) .catch(function (reason) { console.error("JDBotUtil.deleteId(): Error saving revised id collection"); console.error(reason); console.error(filter); console.error(update); }); } } ) .catch(function (reason) { console.error("JDBotUtil.deleteId(): Error deleting %s from ids for %s", idName, users_id); console.error(reason); }); }, /** * Save the user's profile along with the identify information we collect from this channel. * * @param {Person} user - Description of person to be persisted * @param {object} session - Microsoft Botbuilder session object * @return {Promise} */ saveUserProfile: function (user, session) { var oPerson = new Person(); //If we don't already have a channel identity, then create a blank object if (!user.hasOwnProperty("channelIdentities")) { user.channelIdentities = []; } else { //Compensate for the fact that the botbuilder wants to serialize our arrays as higher level objects. user.channelIdentities = oPerson.convertObjectToArray(user.channelIdentities); } //See if we already have this channel in our channelIdentities array var foundChannelIdentity = false; for (var cidx in user.channelIdentities) { var channelIdentity = user.channelIdentities[cidx]; if (channelIdentity.source == session.message.address.channelId) { foundChannelIdentity = true; break; } } //If we don't already have this channel identity, save it to the channelIdentities array. if (!foundChannelIdentity) { channelIdentity = { "source": session.message.address.channelId, "id" : session.message.user.id, "name": session.message.user.name }; user.channelIdentities.push(channelIdentity); } //Make sure the user's options are stored as an array if (user.hasOwnProperty("options")) user.options = oPerson.convertObjectToArray(user.options); //Existing users will have a users_id property on their profile, which is an index into the users collection. if (user.hasOwnProperty("users_id")) { var update = {}; var keys = Object.keys(user); for (var k = 0; k < keys.length; k++) { if (keys[k] !== "channelIdentities") update[keys[k]] = user[keys[k]]; else update["channelIdentities"] = (new Person()).convertObjectToArray(user.channelIdentities); } //Don't try to update an immutable field. if (update.hasOwnProperty("_id")) delete update._id; //Don't add a redundant field delete update.users_id; update = {$set: update}; var filter = {_id: new ObjectId(user.users_id)}; var options = {}; console.info("JDBotUtil.saveUserProfile(): Updating `users` record for %s (%s)", user.email, user.users_id); return mongoUtil.getDb().collection('users').updateOne(filter, update, options); } else { return mongoUtil.getDb().collection('users').insertOne(user); } }, /** * Load a vocabulary of violent acts, including verb tenses and indicators about whether we should ask for * more details, i.e. the use of a weapon. This is done when the server first starts up and is not refreshed * until the server restarts. */ loadViolenceDatabase: function () { var collection = mongoUtil.getDb().collection('verbs'); collection.find({}).forEach(function (verbEntry) { /** * @namespace * @property {object} verbEntry - Entry from MonggoDb verbs collection * @property {string} verbEntry.verb - present-tense verb ("slap") * @property {string} verbEntry.pastTense - past-tense form of verb ("slapped") * @property {string} verbEntry.progressiveTense - progressive-tense form of verb ("slapping") */ violentVerbs[verbEntry.verb] = verbEntry; violentVerbs[verbEntry.pastTense] = verbEntry; violentVerbs[verbEntry.progressiveTense] = verbEntry; }, function (err) { if (err) { console.error("JDBotUtil.loadViolenceDatabase(): Error loading verbs."); console.error(err.stack); } }); }, /** * See whether the utterance from the user may have been intended as a stop word, such as CANCEL, GO BACK, etc. * * @param testPhrase * @returns {*} - Normalized stop word, if found, otherwise null. Stopwords are defined in prompts.js */ getStopWord: function (testPhrase) { if (typeof testPhrase !== "string") return null; var stopWords = Object.keys(prompts.stopWords); for (var i = 0; i < stopWords.length; i++) { var patterns = prompts.stopWords[stopWords[i]].patterns; for (var p = 0; p < patterns.length; p++) { if (testPhrase.match(patterns[p])) return stopWords[i]; } } return null; }, /** * Look at the current state map, the current state, and the existence of a stop word to determine which * dialog the app should display next. Context-sensitive help is a major to-do item. * * The statemaps are defined in statemap.js and the user's current statemap is copied to his or her * userData. * * @param session * @param results - If an empty object (or any object not having a response member) will return the NEXT dialog * @returns {string} - Path of next dialog to call, whether moving forward, quitting, or moving backward */ getNextDialog: function (session, results) { var stateMap = session.userData.stateMap; var thisState = stateMap[session.userData.state]; var nextDialog; if (!results.hasOwnProperty("response")) return thisState.next; var stopWord = this.getStopWord(results.response); switch (stopWord) { //Allow branching based on response to a "choice" prompt. case null: if (results.response.hasOwnProperty("index")) { var idx = results.response.index; if (thisState.hasOwnProperty("choice" + idx)) { return thisState["choice" + idx]; } } nextDialog = thisState.next; break; case "NEXT": nextDialog = thisState.next; break; case "GOBACK": nextDialog = thisState.prev; break; case "CANCEL": nextDialog = thisState.cancel; break; case "HELP": session.send("Help is on the way . . ."); nextDialog = thisState.next; break; default: console.log("JDBotUtil.getNextDialog(): Bad stop word at state %s = %s", session.userData.state, stopWord); nextDialog = thisState.next; } return nextDialog; }, /** * Clean up the userData portion of the session. This mostly helps keep the logs clean. The slow accumulation of * userData is probably not helpful, but frankly I haven't seen any problems. I just want cleaner logs. * * @param session */ cleanupSession: function (session) { delete session.userData.actor; delete session.userData.card; delete session.userData.case; delete session.userData.feedback; delete session.userData.identityPrompt; delete session.userData.paymentAmount; delete session.userData.selectedService; delete session.userData.state; delete session.userData.stateMap; delete session.userData.victim; delete session.userData.violentActs; delete session.userData.violentActIndex; }, makeOrdinal: function (aString) { //remove all non-numeric characters from the string var numericString = aString.replace(/[^0-9]/, ""); //If the string only had non-numeric characters, just return back whatever we originally received. if (numericString.length === 0) return aString; //List of ordinal suffixes the index of which will be the last numeric digit of the court number var ordinalSuffixes = ["th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th"]; var index = numericString.slice(-1, -1) * 1; //coerce into a number return numericString + ordinalSuffixes[index]; }, /** * * @param street * @param csz * @param session * @param callback * * @namespace * @property {string} status - "OK" means we have usable results * @property {[object]} results - All the good stuff we want * @property {[{}]} results[].address_components - Each field of the address * @property {[string]} results[].address_components.types - List of address field types in this component * @property {string} results[].address_components.long_name - Full/Formal value for this field * @property {string} results[].address_components.short_name - Abbreviate value for this field * @property {string} results[].formatted_address - Full address with all fields properly formatted * @property {boolean} results[].partial_match - True or False (not sure what that means all the time) * @property {string} results[].place_id - Google-specific place id. Not sure what this is used for. * @property {[{}]} results[].geometry - Lat/Long information about location and dimension of location * @property {object} results[].geometry.location * @property {number} results[].geometry.location.lat - Latitude of address * @property {number} results[].geometry.location.lng - Longitude of address * @property {string} results[].geometry.location_type - I always see the value "ROOFTOP" * @property {object} results[].geometry.viewport - Coordinates of northeast and southwest points (corners) of address * @property {object} results[].geometry.viewport.northeast - Northeast coordinate of address * @property {number} results[].geometry.viewport.northeast.lat - Latitude of northeast coordinate * @property {number} results[].geometry.viewport.northeast.lng - Longitude of northeast coordinate * @property {object} results[].geometry.viewport.southwest - Southwest coordinate of address * @property {number} results[].geometry.viewport.southwest.lat - Latitude of southwest coordinate * @property {number} results[].geometry.viewport.southwest.lng - Longitude of southwest coordinate * @property {[{string]} results[].types - Array of strings. "subpremise" and "street_address" mean we got a full address * * @namespace * @property {[string]} types * @property {string} long_name * @property {string} short_name * @property {string} formatted_address * @property {[{}]} address_components */ geoCodeAddress: function (street, csz, session, callback) { var myUrl = "https://maps.googleapis.com/maps/api/geocode/json?address=%(address)s&key=%(apikey)s"; var searchAddress = (street + "," + csz).replace(/\s/g, '+'); var query = session.gettext(myUrl, {address: searchAddress, apikey: params.GOOGLE_API_KEY}); https.get(query, function (res) { var body = ''; res.on('data', function (data) { body += data; }); res.on('end', function () { var parsed = JSON.parse(body); var result = {}; result.status = parsed.status; //If not OK, then we have nothing to send back if (parsed.status != "OK") { console.error("JDBotUtil.geoCodeAddress(): Error parsing address."); console.error(parsed); callback(session, result); return; } //Go through each address component and assign to a field in our result for (var i = 0; i < parsed.results[0].address_components.length; i++) { var component = parsed.results[0].address_components[i]; switch (true) { case (component.types.indexOf("administrative_area_level_1") !== -1): result.state = component.short_name; break; case (component.types.indexOf("administrative_area_level_2") !== -1): result.county = component.long_name.replace(/\sCounty/i, ""); break; case (component.types.indexOf("locality") !== -1): result.city = component.long_name; break; case (component.types.indexOf("neighborhood") !== -1): result.neighborhood = component.long_name; break; case (component.types.indexOf("route") !== -1): result.street = component.long_name; break; case (component.types.indexOf("street_number") !== -1): result.streetNumber = component.long_name; break; case (component.types.indexOf("subpremise") !== -1): result.unit = component.long_name; break; case (component.types.indexOf("postal_code") !== -1): result.postalCode = component.long_name; break; case (component.types.indexOf("postal_code_suffix") !== -1): result.postalCodeSuffix = component.long_name; break; } } //An address in an unincorporated area may not have a city name if (!result.hasOwnProperty("city") && result.hasOwnProperty("neighborhood")) result.city = result.neighborhood; if (parsed.results[0].hasOwnProperty("formatted_address")) result.fullAddress = parsed.results[0].formatted_address; var street1 = ""; if (result.hasOwnProperty("streetNumber")) street1 += result.streetNumber; if (result.hasOwnProperty("street")) street1 += " " + result.street; if (result.hasOwnProperty("unit")) street1 += " #" + result.unit; result.street1 = street1.replace(/\s+/g, " ").trim(); var csz = ""; if (result.hasOwnProperty("city")) csz += result.city; if (result.hasOwnProperty("city") && result.hasOwnProperty("state")) csz += ", "; if (result.hasOwnProperty("state")) csz += result.state; if (result.hasOwnProperty("postalCode")) csz += " " + result.postalCode; if (result.hasOwnProperty("postalCode") && result.hasOwnProperty("postalCodeSuffix")) csz += "-" + result.postalCodeSuffix; result.csz = csz; //set quality flags result.hasCompleteStreet = (result.hasOwnProperty("streetNumber") && result.hasOwnProperty("street")); result.hasCompleteCsz = (result.hasOwnProperty("city") && result.hasOwnProperty("state") && result.hasOwnProperty("postalCode")); callback(session, result); }) }); }, lookupCounty: function (country, state, county) { state = state || "TX"; country = country || "US"; var result = {"userString": county}; var myUrl = "https://maps.googleapis.com/maps/api/geocode/json?address=%(address)s&key=%(apikey)s"; var searchAddress = county.replace(/county/i, "").replace(/\b/g, "+") + "+COUNTY,+" + state + ",+" + country; myUrl = myUrl.replace(/%\(address\)s/, searchAddress); myUrl = myUrl.replace(/%\(apikey\)s/, params.GOOGLE_API_KEY); return new Promise(function (resolve, reject) { https.get(myUrl, function (res) { var body = ''; res.on('data', function (data) { body += data; }); res.on('end', function () { var parsed = JSON.parse(body); result.status = parsed.status; if (parsed.status != "OK") { reject(result); return; } //Commented out the loop because for now, I just want to process the first potential match. //for (var a = 0; a < parsed.results.length; a++) { var a = 0; var address = {}; //Go through each address component and assign to a field in our result for (var i = 0; i < parsed.results[a].address_components.length; i++) { var component = parsed.results[a].address_components[i]; switch (true) { case (component.types.indexOf("administrative_area_level_1") !== -1): address.state = component.short_name; break; case (component.types.indexOf("administrative_area_level_2") !== -1): address.county = component.long_name.replace(/\sCounty/i, ""); break; case (component.types.indexOf("country") !== -1): address.countryCode = component.short_name; address.country = component.long_name; break; } } address.formatted_address = parsed.results[a].formatted_address; address.partialMatch = parsed.results[a].hasOwnProperty("partial_match"); } result.address = address; resolve(result); }); }); }); }, gparseAddress: function (session, address, callback) { var myUrl = "https://maps.googleapis.com/maps/api/geocode/json?address=%(address)s&key=%(apikey)s"; var searchAddress = address.replace(/\s/g, '+'); var query = session.gettext(myUrl, {address: searchAddress, apikey: params.GOOGLE_API_KEY}); var result = {"userString": address.replace(/\s+/g, " ").trim()}; var addresses = []; https.get(query, function (res) { var body = ''; res.on('data', function (data) { body += data; }); res.on('end', function () { var parsed = JSON.parse(body); result.status = parsed.status; //If not OK, then we have nothing to send back if (parsed.status != "OK") { console.log("JDBotUtil.gparseAddress(): Error parsing address."); console.log(parsed); callback(session, result); return; } for (var a = 0; a < parsed.results.length; a++) { var address = {}; //Go through each address component and assign to a field in our result for (var i = 0; i < parsed.results[a].address_components.length; i++) { var component = parsed.results[a].address_components[i]; switch (true) { case (component.types.indexOf("administrative_area_level_1") !== -1): address.state = component.short_name; break; case (component.types.indexOf("administrative_area_level_2") !== -1): address.county = component.long_name.replace(/\sCounty/i, ""); break; case (component.types.indexOf("locality") !== -1): address.city = component.long_name; break; case (component.types.indexOf("neighborhood") !== -1): address.neighborhood = component.long_name; break; case (component.types.indexOf("route") !== -1): address.street = component.long_name; break; case (component.types.indexOf("street_number") !== -1): address.streetNumber = component.long_name; break; case (component.types.indexOf("subpremise") !== -1): address.unit = component.long_name; break; case (component.types.indexOf("country") !== -1): address.countryCode = component.short_name; address.country = component.long_name; break; case (component.types.indexOf("postal_code") !== -1): address.postalCode = component.long_name; break; case (component.types.indexOf("postal_code_suffix") !== -1): address.postalCodeSuffix = component.long_name; break; } } //An address in an unincorporated area may not have a city name if (!address.hasOwnProperty("city") && address.hasOwnProperty("neighborhood")) address.city = address.neighborhood; if (parsed.results[a].hasOwnProperty("formatted_address")) address.fullAddress = parsed.results[a].formatted_address; var street1 = ""; if (address.hasOwnProperty("streetNumber")) street1 += address.streetNumber; if (address.hasOwnProperty("street")) street1 += " " + address.street; if (address.hasOwnProperty("unit")) street1 += " #" + address.unit; address.street1 = street1.replace(/\s+/g, " ").trim(); var csz = ""; if (address.hasOwnProperty("city")) csz += address.city; if (address.hasOwnProperty("city") && address.hasOwnProperty("state")) csz += ", "; if (address.hasOwnProperty("state")) csz += address.state; if (address.hasOwnProperty("postalCode")) csz += " " + address.postalCode; if (address.hasOwnProperty("postalCode") && address.hasOwnProperty("postalCodeSuffix")) csz += "-" + address.postalCodeSuffix; address.csz = csz; //set quality flags address.hasCompleteStreet = (address.hasOwnProperty("streetNumber") && address.hasOwnProperty("street")); address.hasCompleteCsz = (address.hasOwnProperty("city") && address.hasOwnProperty("state") && address.hasOwnProperty("postalCode")); addresses.push(address); } result.addresses = addresses; callback(session, result); }) }); }, getCase: function (causeNumber, county, state, country) { if (!causeNumber) throw "jdBotUtil.getCase(): causeNumber is required."; if (!county) throw "jdBotUtil.getCase(): county is required"; state = state || "TX"; country = country || "USA"; return new Promise(function (resolve, reject) { var collection = mongoUtil.getDb().collection('cases'); var query = {country: country, causeNumber: causeNumber, county: county, state: state}; collection.find(query).limit(1).toArray() .then(function (result) { resolve(result); }) .catch(function (reason) { console.error("JdBotUtil.getCase(): Error retrieving case."); console.error(reason); reject(reason); }); }); }, /** * Take a matterType abbreviation, e.g. DIVC, DIVN, and return a description of that type of case. * * @param {string} matterType - Matter Type abbreviation, probably from the cases collection * @returns {*} */ getMatterTypeDescription: function (matterType) { for (var cidx in prompts.causes.all) { var coa = prompts.causes.all[cidx]; if (coa.id == matterType) { coa.description = cidx; return coa; } } return {}; }, /** * Try to determine the type of court and the court number or name based on the cause number. Each county formats * its cause numbers differently. Most seem to be "intelligent keys" in that they contain at least the court * number and often the court type. Some, like DALLAS COUNTY, make it easy to figure out the court TYPE, but leave * the court NUMBER obscured. * * @param {string} causeNumber - The cause number from the pleadings or citation. REQUIRED. * @param {string} county - The county in which the case was filed. REQUIRED, but defaults to "COLLIN" * @param {string=} state - The two-character state code (e.g. AR, CA, TX, NY). Defaults to "TX" * @param {string=} country - The two-character country code (e.g. US, UK, DE). Defaults to "US" * * The resolve method produces an object that has these properties: * * type: False means we could not find any applicable rules OTHERWISE the type of court, e.g. "District Court" * courtNumber: False means we could not determine the court number OTHERWISE the court number, e.g. 470 * county: The county we searched for * state: The state we searched for * country: The country we searched for * * @returns {Promise} */ recognizeCourt: function (causeNumber, county, state, country) { var myCounty = (county || 'COLLIN').toUpperCase(); var myState = (state || 'TX').toUpperCase(); var myCountry = (country || 'US').toUpperCase(); var result = { "type" : false, "courtNumber": false, "county" : myCounty, "state" : myState, "country" : myCountry, "ruleCount" : 0, "causeNumber": causeNumber }; var ruleIndex = myCountry + ":" + myState + ":" + myCounty; var query = {"county": ruleIndex}; var projection = {"rules": 1}; var collection = mongoUtil.getDb().collection('courtRecognitionRules'); var processRules = function (docs) { try { //If we don't have any rules for this political geography, exit with a blank result if (docs.length === 0) { result.message = "Docs length was zero - no rules found."; return result; } var rules = docs[0].rules; result.ruleCount = rules.length; //Go through the recognition rules var i; var courtNumber = false; var foundMatch = false; for (i = 0; i < rules.length && foundMatch === false; i++) { //Check for a match /** @property {RegExp} pattern - Regex that if matched fires this rule */ var matches = causeNumber.match(rules[i].pattern); //If we have a match . . . if (matches !== null) { foundMatch = true; //Dereference the rule to make it easier to work with. var rule = rules[i]; //See if we got a capture result. At this time, the only capture group in the patterns is for the //court number. if (matches.length >= 2) { //We captured the court number. See if it needs to be translated. //E.G. in Dallas County, District Court #256 is designated as "Z" rather than "256" in the cause number /** @property {[]} translations - maps cause number segment to actual court number */ if (rule.hasOwnProperty("translations")) { //Translate the court number. If we got to here, it means we HAD to translate the court number. //Either return the translated court number OR false, which indicates that required translation failed. //Returning this false value signals to the invoking dialog the need to ask the human to try to //find a court number. courtNumber = rule.translations.hasOwnProperty(matches[1]) ? rule.translations[matches[1]] : false; } else { courtNumber = matches[1]; } } result.type = rule.type; result.courtNumber = courtNumber; break; } } return result; } catch (err) { console.error("JDBotUtil.recognizeCourt(): Error recognizing court [1] ('%s', '%s', '%s', '%s'", causeNumber, myCounty, myState, myCountry); console.error(err.stack); } }; return new Promise((resolve, reject) => { collection.find(query).project(projection).limit(1).toArray((err, docs) => { if (err) { console.error("JDBotUtil.recognizeCourt(): Error recognizing court [2] ('%s', '%s', '%s', '%s'", causeNumber, myCounty, myState, myCountry); console.error(err); reject(result); } else { resolve(processRules(docs)); } }); }); }, getRandomElement(arr) { return arr[Math.floor(Math.random() * arr.length)]; }, /** * Implements the Luhn algorighm for validating a number based on it's ending check-digit. Works for * credit card numbers, US Social Security Numbers, etc. Rf. https://en.wikipedia.org/wiki/Luhn_algorithm * * @param value * @returns {boolean} */ validateCheckDigit: function (value) { //Reject input containing other than digits, space, and dash if (!value.match(/^[0-9-\s]+$/)) return false; //Luhn[2] Algorightm from https://gist.github.com/DiegoSalazar/4075533 //TJD: I cleaned this up just a little. var nCheck = 0; var nDigit = 0; var cDigit = 0; var bEven = false; value = value.replace(/\D/g, ""); for (var n = value.length - 1; n >= 0; n--) { cDigit = value.charAt(n); nDigit = parseInt(cDigit, 10); if (bEven) { if ((nDigit *= 2) > 9) nDigit -= 9; } nCheck += nDigit; bEven = !bEven; } return (nCheck % 10) == 0; } };