All files / src alerts.coffee

33.33% Statements 38/114
0% Branches 0/23
0% Functions 0/54
34.23% Lines 38/111
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 3621x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x     1x   1x                                                                               1x                                                                                 1x   1x   1x                     1x             1x                           1x               1x                                                                   1x                           1x               1x                             1x                 1x                                                         1x                                   1x                                                                                     1x                                                                       1x         1x     1x 1x 1x  
config = require "./config/config"
config.alerts = config.get('alerts')
logger = require "winston"
contact = require './contact'
moment = require 'moment'
Q = require 'q'
Channels = require('./model/channels')
Channel = Channels.Channel
Event = require('./model/events').Event
ContactGroup = require('./model/contactGroups').ContactGroup
Alert = require('./model/alerts').Alert
User = require('./model/users').User
authorisation = require('./middleware/authorisation')
utils = require './utils'
_ = require 'lodash'
 
 
trxURL = (trx) -> "#{config.alerts.consoleURL}/#/transactions/#{trx.transactionID}"
 
statusTemplate = (transactions, channel, alert) ->
  plain: ->
    """
    OpenHIM Transactions Alert
 
    The following transaction(s) have completed with status #{alert.status} on the OpenHIM instance running on #{config.alerts.himInstance}:
    Channel - #{channel.name}
    #{(transactions.map (trx) -> trxURL trx).join '\n'}
 
    """
  html: ->
    text = """
      <html>
        <head></head>
        <body>
          <h1>OpenHIM Transactions Alert</h1>
          <div>
            <p>The following transaction(s) have completed with status <b>#{alert.status}</b> on the OpenHIM instance running on <b>#{config.alerts.himInstance}</b>:</p>
            <table>
              <tr><td>Channel - <b>#{channel.name}</b></td></td>\n
      """
    text += (transactions.map (trx) -> "        <tr><td><a href='#{trxURL trx}'>#{trxURL trx}</a></td></tr>").join '\n'
    text += '\n'
    text += """
            </table>
          </div>
        </body>
      </html>
      """
  sms: ->
    text = "Alert - "
    if transactions.length > 1
      text += "#{transactions.length} transactions have"
    else if transactions.length is 1
      text += "1 transaction has"
    else
      text += "no transactions have"
    text += " completed with status #{alert.status} on the OpenHIM running on #{config.alerts.himInstance} (#{channel.name})"
 
 
maxRetriesTemplate = (transactions, channel, alert) ->
  plain: ->
    """
    OpenHIM Transactions Alert - #{config.alerts.himInstance}
 
    The following transaction(s) have been retried #{channel.autoRetryMaxAttempts} times, but are still failing:
 
    Channel - #{channel.name}
    #{(transactions.map (trx) -> trxURL trx).join '\n'}
 
    Please note that they will not be retried any further by the OpenHIM automatically.
    """
  html: ->
    text = """
      <html>
        <head></head>
        <body>
          <h1>OpenHIM Transactions Alert - #{config.alerts.himInstance}</h1>
          <div>
            <p>The following transaction(s) have been retried <b>#{channel.autoRetryMaxAttempts}</b> times, but are still failing:</p>
            <table>
              <tr><td>Channel - <b>#{channel.name}</b></td></td>\n
      """
    text += (transactions.map (trx) -> "        <tr><td><a href='#{trxURL trx}'>#{trxURL trx}</a></td></tr>").join '\n'
    text += '\n'
    text += """
            </table>
            <p>Please note that they will not be retried any further by the OpenHIM automatically.</p>
          </div>
        </body>
      </html>
      """
  sms: ->
    text = "Alert - "
    if transactions.length > 1
      text += "#{transactions.length} transactions have"
    else if transactions.length is 1
      text += "1 transaction has"
    text += " been retried #{channel.autoRetryMaxAttempts} times but are still failing on the OpenHIM on #{config.alerts.himInstance} (#{channel.name})"
 
 
getAllChannels = (callback) -> Channel.find {}, callback
 
findGroup = (groupID, callback) -> ContactGroup.findOne _id: groupID, callback
 
findTransactions = (channel, dateFrom, status, callback) ->
  Event
    .find {
      created: $gte: dateFrom
      channelID: channel._id
      event: 'end'
      status: status
    }, { 'transactionID' }
    .hint created: 1
    .exec callback
 
countTotalTransactionsForChannel = (channel, dateFrom, callback) ->
  Event.count {
    created: $gte: dateFrom
    channelID: channel._id
    event: 'end'
  }, callback
 
findOneAlert = (channel, alert, dateFrom, user, alertStatus, callback) ->
  criteria = {
    timestamp: { "$gte": dateFrom }
    channelID: channel._id
    condition: alert.condition
    status: if alert.condition is 'auto-retry-max-attempted' then '500' else alert.status
    alertStatus: alertStatus
  }
  criteria.user = user if user
  Alert
    .findOne criteria
    .exec callback
 
 
findTransactionsMatchingCondition = (channel, alert, dateFrom, callback) ->
  if not alert.condition or alert.condition is 'status'
    findTransactionsMatchingStatus channel, alert, dateFrom, callback
  else if alert.condition is 'auto-retry-max-attempted'
    findTransactionsMaxRetried channel, alert, dateFrom, callback
  else
    callback new Error "Unsupported condition '#{alert.condition}'"
 
findTransactionsMatchingStatus = (channel, alert, dateFrom, callback) ->
  pat = /\dxx/.exec alert.status
  if pat
    statusMatch = "$gte": alert.status[0]*100, "$lt": alert.status[0]*100+100
  else
    statusMatch = alert.status
 
  dateToCheck = dateFrom
  # check last hour when using failureRate
  dateToCheck = moment().subtract(1, 'hours').toDate() if alert.failureRate?
 
  findTransactions channel, dateToCheck, statusMatch, (err, results) ->
    if not err and results? and alert.failureRate?
      # Get count of total transactions and work out failure ratio
      _countStart = new Date()
      countTotalTransactionsForChannel channel, dateToCheck, (err, count) ->
        logger.debug ".countTotalTransactionsForChannel: #{new Date()-_countStart} ms"
 
        return callback err, null if err
 
        failureRatio = results.length/count*100.0
        if failureRatio >= alert.failureRate
          findOneAlert channel, alert, dateToCheck, null, 'Completed', (err, userAlert) ->
            return callback err, null if err
            # Has an alert already been sent this last hour?
            if userAlert?
              callback err, []
            else
              callback err, utils.uniqArray results
        else
          callback err, []
    else
      callback err, results
 
findTransactionsMaxRetried = (channel, alert, dateFrom, callback) ->
  Event
    .find {
      created: $gte: dateFrom
      channelID: channel._id
      event: 'end'
      status: 500
      autoRetryAttempt: channel.autoRetryMaxAttempts
    }, { 'transactionID' }
    .hint created: 1
    .exec (err, transactions) ->
      return callback err if err
      callback null, _.uniqWith transactions, (a, b) -> a.transactionID.equals b.transactionID
 
calcDateFromForUser = (user) ->
  if user.maxAlerts is '1 per hour'
    dateFrom = moment().subtract(1, 'hours').toDate()
  else if user.maxAlerts is '1 per day'
    dateFrom = moment().startOf('day').toDate()
  else
    null
 
userAlreadyReceivedAlert = (channel, alert, user, callback) ->
  if not user.maxAlerts or user.maxAlerts is 'no max'
    # user gets all alerts
    callback null, false
  else
    dateFrom = calcDateFromForUser user
    return callback "Unsupported option 'maxAlerts=#{user.maxAlerts}'" if not dateFrom
 
    findOneAlert channel, alert, dateFrom, user.user, 'Completed', (err, userAlert) ->
      callback err ? null, if userAlert then true else false
 
# Setup the list of transactions for alerting.
#
# Fetch earlier transactions if a user is setup with maxAlerts.
# If the user has no maxAlerts limit, then the transactions object is returned as is.
getTransactionsForAlert = (channel, alert, user, transactions, callback) ->
  if not user.maxAlerts or user.maxAlerts is 'no max'
    callback null, transactions
  else
    dateFrom = calcDateFromForUser user
    return callback "Unsupported option 'maxAlerts=#{user.maxAlerts}'" if not dateFrom
 
    findTransactionsMatchingCondition channel, alert, dateFrom, callback
 
sendAlert = (channel, alert, user, transactions, contactHandler, done) ->
  User.findOne { email: user.user }, (err, dbUser) ->
    return done err if err
    return done "Cannot send alert: Unknown user '#{user.user}'" if not dbUser
 
    userAlreadyReceivedAlert channel, alert, user, (err, received) ->
      return done err, true if err
      return done null, true if received
 
      logger.info "Sending alert for user '#{user.user}' using method '#{user.method}'"
 
      getTransactionsForAlert channel, alert, user, transactions, (err, transactionsForAlert) ->
        template = statusTemplate transactionsForAlert, channel, alert
        if alert.condition is 'auto-retry-max-attempted'
          template = maxRetriesTemplate transactionsForAlert, channel, alert
 
        if user.method is 'email'
          plainMsg = template.plain()
          htmlMsg = template.html()
          contactHandler 'email', user.user, 'OpenHIM Alert', plainMsg, htmlMsg, done
        else if user.method is 'sms'
          return done "Cannot send alert: MSISDN not specified for user '#{user.user}'" if not dbUser.msisdn
 
          smsMsg = template.sms()
          contactHandler 'sms', dbUser.msisdn, 'OpenHIM Alert', smsMsg, null, done
        else
          return done "Unknown method '#{user.method}' specified for user '#{user.user}'"
 
# Actions to take after sending an alert
afterSendAlert = (err, channel, alert, user, transactions, skipSave, done) ->
  logger.error err if err
 
  if not skipSave
    alert = new Alert
      user: user.user
      method: user.method
      channelID: channel._id
      condition: alert.condition
      status: if alert.condition is 'auto-retry-max-attempted' then '500' else alert.status
      alertStatus: if err then 'Failed' else 'Completed'
 
    alert.save (err) ->
      logger.error err if err
      done()
  else
    done()
 
sendAlerts = (channel, alert, transactions, contactHandler, done) ->
  # Each group check creates one promise that needs to be resolved.
  # For each group, the promise is only resolved when an alert is sent and stored
  # for each user in that group. This resolution is managed by a promise set for that group.
  #
  # For individual users in the alert object (not part of a group),
  # a promise is resolved per user when the alert is both sent and stored.
  promises = []
 
  _alertStart = new Date()
  if alert.groups
    for group in alert.groups
      groupDefer = Q.defer()
      findGroup group, (err, result) ->
        if err
          logger.error err
          groupDefer.resolve()
        else
          groupUserPromises = []
 
          for user in result.users
            do (user) ->
              groupUserDefer = Q.defer()
              sendAlert channel, alert, user, transactions, contactHandler, (err, skipSave) ->
                afterSendAlert err, channel, alert, user, transactions, skipSave, -> groupUserDefer.resolve()
              groupUserPromises.push groupUserDefer.promise
 
          (Q.all groupUserPromises).then -> groupDefer.resolve()
      promises.push groupDefer.promise
 
  if alert.users
    for user in alert.users
      do (user) ->
        userDefer = Q.defer()
        sendAlert channel, alert, user, transactions, contactHandler, (err, skipSave) ->
          afterSendAlert err, channel, alert, user, transactions, skipSave, -> userDefer.resolve()
        promises.push userDefer.promise
 
  (Q.all promises).then ->
    logger.debug ".sendAlerts: #{new Date()-_alertStart} ms"
    done()
 
 
alertingTask = (job, contactHandler, done) ->
  job.attrs.data = {} if not job.attrs.data
 
  lastAlertDate = job.attrs.data.lastAlertDate ? new Date()
 
  _taskStart = new Date()
  getAllChannels (err, results) ->
    promises = []
 
    for channel in results
      if Channels.isChannelEnabled channel
 
        for alert in channel.alerts
          do (channel, alert) ->
            deferred = Q.defer()
 
            _findStart = new Date()
            findTransactionsMatchingCondition channel, alert, lastAlertDate, (err, results) ->
              logger.debug ".findTransactionsMatchingStatus: #{new Date()-_findStart} ms"
 
              if err
                logger.error err
                deferred.resolve()
              else if results? and results.length>0
                sendAlerts channel, alert, results, contactHandler, -> deferred.resolve()
              else
                deferred.resolve()
 
            promises.push deferred.promise
 
    (Q.all promises).then ->
      job.attrs.data.lastAlertDate = new Date()
      logger.debug "Alerting task total time: #{new Date()-_taskStart} ms"
      done()
 
 
setupAgenda = (agenda) ->
  agenda.define 'generate transaction alerts', (job, done) -> alertingTask job, contact.contactUser, done
  agenda.every "#{config.alerts.pollPeriodMinutes} minutes", 'generate transaction alerts'
 
 
exports.setupAgenda = setupAgenda
 
if process.env.NODE_ENV == "test"
  exports.findTransactionsMatchingStatus = findTransactionsMatchingStatus
  exports.findTransactionsMaxRetried = findTransactionsMaxRetried
  exports.alertingTask = alertingTask