medic / cht-core

The CHT Core Framework makes it faster to build responsive, offline-first digital health apps that equip health workers to provide better care in their communities. It is a central resource of the Community Health Toolkit.
https://communityhealthtoolkit.org
GNU Affero General Public License v3.0
441 stars 211 forks source link

Trigger an SMS to be sent when multiple reports that meet certain conditions are sent in from the same community #3416

Closed sglangevin closed 7 years ago

sglangevin commented 7 years ago

The Community Based Disease Surveillance (CBDS) use case includes a variety of SMS alerts that go out to specific people in the health system who are responsible for monitoring and responding to any potential outbreaks. Most of these alerts are triggered by one report of a certain set of symptoms. In a few cases, the SMS needs to be triggered when a certain threshold of reports with a certain set of symptoms is reported. For example, trigger an SMS to be sent when a CHW reports 5 cases of non-bloody diarrhea or if 5 cases are reported by multiple CHWs in the same community.

A few other examples in this doc: https://docs.google.com/spreadsheets/d/10pi7EkKB73zqnR4eBarI88VbcSGfc2aZ77piHcL1HbM/edit#gid=0

@abbyad and I discussed this briefly and thought that a custom Sentinel transition might do the trick, but I am interested to see what other ways we could implement this.

cc @samkanga

sglangevin commented 7 years ago

I gathered some more information about this feature request after talking with @samkanga and @katanu:

This feature is required for a deployment in July.

estellecomment commented 7 years ago

@katanu @sglangevin @samkanga I think this also needs to have a time window, no? E.g. 3 death reports in the same area in the last month. (without a time window, this will be triggered eventually for all areas, once enough people are dead)

sglangevin commented 7 years ago

@estellecomment the time window is 1 week for this project. So if you get a death report today, you have to get 2 more death reports (total of 3) within a week from today from the same community.

estellecomment commented 7 years ago

If the reports to count are all from the same form, then we can use the existing alerts feature, with a really complicated condition :

// Function returning true if you should count this report towards the threshold.
var isAProblem = function(report) {
  return (report.area === FORM_CODE(0).area && // same CHW area
    report.symptoms === REALLY_BAD_SYMPTOMS); // whatever checks you want to run
};

// Function returning true if the report is within TIME_WINDOW of the current report.
var isWithinTimeWindow = function(report) {
  return report.submission_date > FORM_CODE(0).submission_date + TIME_WINDOW;
};

var problemReportsCounter = 0;
var i = 0;
var report;
do {
  report = FORM_CODE(i);
  if (!isWithinTimeWindow(report)) {
    return false; // break the loop, we’ve gone further back than the time window
  }

  if (isAProblem(report)) {
    problemReportsCounter++;
  }
  i++;
} while (report);

return problemReportsCounter >= THRESHOLD;

but if this is a common use case then we want something simpler to configure, and if the reports come from several different forms then we'll have to write custom code anyway.

In that case, we can add an app_settings field called thresholdAlerts or whatever, something like :

{
  isReportCounted : function(report, currentForm) {
    return report.area === currentForm.area && // from same area
      (report.form === FORM_CODE_1 &&
          report.somefield === REALLY_BAD_VALUE)  ||
      (report.form === FORM_CODE_2 &&
         report.someOtherfield === OTHER_BAD_VALUE);
  },
  timeWindowInDays : 7,
  numReportsThreshold : 3
}

and add a sentinel which does something similar to the current conditional_alerts.js (or reuse the same app_setting and transition with extra bells and whistles, whichever)

estellecomment commented 7 years ago

Update : the reports can be from several forms.

So I'm going for adding a new transition whose config will look something like this :

{
  timeWindowInDays : 7, // look through all reports from the last 7 days
  isReportCounted : function(report, lastReport) { 
    // report : a report from the last 7 days
    // lastReport : the report that triggered the transition
    return report.contact.parish === lastReport.contact.parish && // from same area
          (report.form === FORM_CODE_1 &&
             report.somefield === REALLY_BAD_VALUE)  ||
          (report.form === FORM_CODE_2 &&
             report.someOtherfield === OTHER_BAD_VALUE);
  },
  numReportsThreshold : 3, // if we count 3 reports, then trigger the alert
  message: ‘3 patients with big problem in 7 days, reported at {{countedReports[0].contact.parent.parish}}. Report by {{countedReports[0].contact.name}} ({{countedReports[0].contact.phone}}) for patient {{countedReports[0].patient_id}}. Report by {{countedReports[1].contact.name}} <...>’,
  messageRecipients: [ 
    ‘+254777888999’, // super-supervisor
    ‘countedReports[0].contact.parent.parent.contact.phone’ // contact for the district that the CHW area is in
  ]
}

Specifying message recipients by parish is trickier, this is not part of our standard hierarchy. I also don't know who in the parish we're targeting, can we have examples @sglangevin? Is there a parish contact person (and if so where is it specified)? Are we sending to everyone in the parish?

Inside, we'll

We might have to rehydrate the reports for the isReportCounted func too, depending on the func. That could be a lof of queries though. If we don't, then we have to specify for the people configuring that the reports in isReportCounted are not hydrated, while the ones in countedReports are, which is pretty awful UX.

sglangevin commented 7 years ago

@estellecomment I think we were still waiting to confirm that the parish is the community in which we need to count reports. @samkanga @katanu can you please confirm this?

@samkanga and @katanu can also give more information on who receives the alert. I believe it will be a group of people which might differ by community. @samkanga @katanu can you please help provide more info so we can get this feature built?

samkanga commented 7 years ago

@estellecomment we have done some side enquiries as we wait for confirmation from LG on the level for reports count. The village is the lowest administrative unit followed by a parish. On average, a parish has between 100 to 150 families. In terms of who receives the alert, we presently have identified the surveillance person at the health facility (serves a parish), while at higher levels we have the district health office and the national level (EoC) who would also ideally need to receive the alert. The LG branch managers also receive the alerts.

sglangevin commented 7 years ago

@samkanga we don't currently track the village that each CHP covers. If we are going to count these forms by village, then we would need to add a field (village) for every CHP area. Otherwise we should start with counting by parish, since that information is already there. Does each CHP cover a particular village?

estellecomment commented 7 years ago

@samkanga @sglangevin The info I need to implement is : for each of these people that need to me messaged, where is the person stored in our docs? Given a report, how do you get to the message recipient? e.g. report.contact.parent.parishContact for instance.

If they can't be specified that way, then we might have a problem.

samkanga commented 7 years ago

@sglangevin we don't need to add the village field since CHP areas may span more than one village. It is in order to conduct the count by parish. @estellecomment am not very clear on the organization of our docs. However, a recipient need not exist as a user on the system to receive an alert. The plan is to have a capability to capture at least the cell numbers of the recipients so that a message can be sent to those contacts once generated.

sglangevin commented 7 years ago

@estellecomment I talked with @samkanga and here is what we agreed on:

There are about 10 people who need to receive SMS alerts for a particular branch. Each branch contains multiple parishes but only 1 link facility (first level of MOH hierarchy who receive alerts). In addition to the link facility people, there are district-level and national-level contacts who receive the alerts, as well as LG branch managers. The district-level and national-level contacts technically are above the branch level in the hierarchy, but we don't have levels in our system for those people.

So one option would be to create all 10 contacts at each branch and send to all of them. The downside of that is some of the 10 will be the same across multiple branches and we aren't allowed to have multiple contacts in the system with the same phone number. Another option might be to have a way of specifying a list of phone numbers for each branch without actually creating contacts. As long as we can find a way to specify the list of contacts for each branch and send to them, we're good, so maybe you have a better idea?

And to be clear, we are still going to count reports within a particular parish, so we should trigger these messages based on meeting the conditions within a specific parish.

estellecomment commented 7 years ago

we are still going to count reports within a particular parish

Yup, that's fine : report.contact.parent.parish is the parish name (right?), so you can compare the parish name for two reports.

a way of specifying a list of phone numbers for each branch without actually creating contacts

We don't actually need to have a person doc in the system for the message recipients. We can

Does that cover all cases? Anything I'm missing?

sglangevin commented 7 years ago

Cool, yeah, that sounds like it will cover all bases. I think it would be fine to put all of the contacts (branch, district and national) in the branch doc alertRecipients field. Or if you want to make it possible to put national contacts directly in app_settings, that would also be fine, and we could put just the branch and district contacts in the branch doc alertRecipients field.

garethbowen commented 7 years ago

@estellecomment I haven't made any further progress on this, just cleaned up what you had and got the tests passing. Back to you.

estellecomment commented 7 years ago

Cleaned up and ready for review!

Code in sentinel : https://github.com/medic/medic-sentinel/pull/138 Documentation in webapp : https://github.com/medic/medic-webapp/pull/3609/ More documentation in medic-docs : https://github.com/medic/medic-docs/pull/26

(My script to assign random reviewers : return '@garethbowen')

estellecomment commented 7 years ago

Still todo :

estellecomment commented 7 years ago

No DB shards could be opened error was because I was querying the view in parallel, not in series, and if there's more than 25 queries in parallel (from testing by hand, could be approx), it throws that error.

estellecomment commented 7 years ago

Newlines are a CSS issue, filing separately : https://github.com/medic/medic-webapp/issues/3612

garethbowen commented 7 years ago

Back to you @estellecomment

ngaruko commented 7 years ago

Any clues on how to AT this @sglangevin @estellecomment ?

abbyad commented 7 years ago

I have already been acceptance testing this as part of #3715. Will share a config to help you get started with this feature.

abbyad commented 7 years ago

I see this working with the following config:

  "multi_report_alerts": [
    {
      "name": "test",
      "num_reports_threshold": 2,
      "time_window_in_days": 7,
      "is_report_counted": "function(report, latestReport) { return latestReport.contact.parent.parent._id === report.contact.parent.parent._id; }",
      "message": "[nr.0._id                                 {{new_reports.0._id}}] [nr.0.c._id                         {{new_reports.0.contact._id}}][nr.0.c.name                        {{new_reports.0.contact.name}}][nr.0.c.parent._id                  {{new_reports.0.contact.parent._id}}][nr.0.c.parent.name                 {{new_reports.0.contact.parent.name}}][nr.0.c.parent.parent._id           {{new_reports.0.contact.parent.parent._id}}][nr.0.c.parent.parent.name          {{new_reports.0.contact.parent.parent.name}}][nr.0.c.parent.parent.parent._id    {{new_reports.0.contact.parent.parent.parent._id}}][nr.0.c.parent.parent.parent.name   {{new_reports.0.contact.parent.parent.parent.name}}][nr.0.c.parent.parent.parent.contact._id    {{new_reports.0.contact.parent.parent.parent.contact._id}}][nr.0.c.parent.parent.parent.contact.phone   {{new_reports.0.contact.parent.parent.parent.contact.phone}}] ----- [nr.1._id                                 {{new_reports.1._id}}] [nr.1.c._id                         {{new_reports.1.contact._id}}][nr.1.c.name                        {{new_reports.1.contact.name}}][nr.1.c.parent._id                  {{new_reports.1.contact.parent._id}}][nr.1.c.parent.name                 {{new_reports.1.contact.parent.name}}][nr.1.c.parent.parent._id           {{new_reports.1.contact.parent.parent._id}}][nr.1.c.parent.parent.name          {{new_reports.1.contact.parent.parent.name}}][nr.1.c.parent.parent.parent._id    {{new_reports.1.contact.parent.parent.parent._id}}][nr.1.c.parent.parent.parent.name   {{new_reports.1.contact.parent.parent.parent.name}}][nr.1.c.parent.parent.parent.contact._id    {{new_reports.1.contact.parent.parent.parent.contact._id}}][nr.1.c.parent.parent.parent.contact.phone   {{new_reports.1.contact.parent.parent.parent.contact.phone}}] Report by {{new_reports.0.contact.name}} ({{new_reports.0.contact.phone}}) for patient {{new_reports.0.fields.patient_id}}",
      "recipients": [
        "+123456789",
        "new_report.contact.phone",
        "new_report.contact.parent.parent.contact.phone"
      ],
      "forms": [
        "D",
        "delivery"
      ]
    },
    {
      "name": "flag",
      "num_reports_threshold": 3,
      "time_window_in_days": 7,
      "is_report_counted": "function(report, latestReport) { return latestReport.contact.parent.parent._id === report.contact.parent.parent._id; }",
      "message": "{{num_counted_reports}} patients with {{alert_name}} reports in {{new_reports.0.contact.parent.parent.name}} over the last {{time_window_in_days}} days. New reports from {{#new_reports}}{{contact.parent.name}}, {{/new_reports}}since the last alert.",
      "recipients": [
        "+14169876543",
        "new_report.contact.phone",
        "new_report.contact.parent.parent.contact.phone"
      ],
      "forms": [
        "F"
      ]
    }
  ]
sglangevin commented 7 years ago

Thanks @abbyad, this is great!

@estellecomment: For the real project that is using this feature, it's not just the form submission that matters. It's forms that meet certain conditions, so for example, an assessment form has a field blood_in_diarrhea = 'true'. Where/how are we able to specify such conditions? Can we do that in the forms array?

abbyad commented 7 years ago

That would be done in the is_report_counted function.

For example:

      "is_report_counted": "function(report, latestReport) { return latestReport.contact.parent.parent._id === report.contact.parent.parent._id && latestReport.fields.blood_in_diarrhea == true; }",
sglangevin commented 7 years ago

Ok cool. Thanks! I think we can move this to Ready, unless we wanted to create some config for the project that requested this feature? I'd also be happy to assign that to the tech lead who can go from the config posted here.

abbyad commented 7 years ago

Ya, from what I saw in the request this feature is ready enough to meet the needs of the project. We can open new issue if we learn of anything more needed while configuring the project.

sglangevin commented 7 years ago

Great! Moving to ready.

estellecomment commented 7 years ago

:)

this feature is ready enough to meet the needs of the project

Applying the 4.63 - Ready Enough tag

Thanks for the review and troubleshooting help!