<commit_before>// Possible configuration items:
//
// * translation.<lang>:
//     Jed translation JSON for language (e.g. sw). Optional. If ommitted,
//     untranslated strings are used.
//
// * config:
//     * qa:
//         Set to true to turn on QA features. Default is false.
//     * swb_api:
//         Dictionary of username, password and url of the Switchboard API.
//         If ommitted, the dummy API is used instead.
//     * sms_tag:
//         Two element list of [pool, tag] giving the Go endpoint to send SMSes
//         out via. If ommitted, SMSes are not sent.
//     * metric_store:
//         Name of the metric store to use. If omitted, metrics are sent
//         to the metric store named 'default'.
//     * valid_user_addresses:
//         JSON list of allowed from_addr regular expression patterns.
//         Optional. If omitted, all from_addr values are allowed.
//
// It is suspected that the Vodacom Tz prefixes are: 255743 - 6, 25575 and
// 25576.
//
// Metrics produced:
//
// * ussd_sessions
// * unique_users
// * first_session_completed
// * second_session_completed
// * sessions_taken_to_register (average)
// * session_new_in.<state-name>
// * session_closed_in.<state-name>
// * possible_timeout_in.<state-name>
// * state_entered.<state-name>
// * state_exited.<state-name>

var vumigo = require("vumigo_v01");
var jed = require("jed");

if (typeof api === "undefined") {
    // testing hook (supplies api when it is not passed in by the real sandbox)
    var api = this.api = new vumigo.dummy_api.DummyApi();
}

var Promise = vumigo.promise.Promise;
var success = vumigo.promise.success;
var maybe_promise = vumigo.promise.maybe_promise;
var State = vumigo.states.State;
var Choice = vumigo.states.Choice;
var ChoiceState = vumigo.states.ChoiceState;
var LanguageChoice = vumigo.states.LanguageChoice;
var PaginatedChoiceState = vumigo.states.PaginatedChoiceState;
var FreeText = vumigo.states.FreeText;
var EndState = vumigo.states.EndState;
var InteractionMachine = vumigo.state_machine.InteractionMachine;
var StateCreator = vumigo.state_machine.StateCreator;


function DummySwitchboardApi(im) {

    var self = this;

    self.im = im;

    self.list_cadres = function() {
        var p = new Promise();
        p.callback([
            // Medical Specialist, AMO and Dental Specialist
            // use numerical IDs to match default IDs for cadres
            // with specialties.
            {id: 1, text: "Medical Specialist"},
            {id: "mo", text: "MO"}, // Medical Officer
            {id: 60, text: "AMO"}, // Assistant Medical Officer
            {id: "co", text: "CO"}, // Clinical Officer
            {id: "aco", text: "ACO"}, // Assistant Clinical Officer
            {id: 67, text: "Dental Specialist"},
            {id: "do", text: "Dental Officer"},
            {id: "ado", text: "ADO"}, // Assitant Dental Officer
            {id: "dt", text: "Dental Therapist"}
        ]);
        return p;
    };

    self.list_districts = function(query) {
        var p = new Promise();
        p.callback([
            {id: "kigoma-mc", text: "Kigoma MC"},
            {id: "kigoma-dc", text: "Kigoma DC"},
            {id: "kasulu-dc", text: "Kasulu DC"}
        ]);
        return p;
    };

    self.list_facility_types = function() {
        var p = new Promise();
        var _ = self.im.i18n;
        p.callback([
            {id: "hospital", text: _.gettext("Hospital")},
            {id: "health-centre", text: _.gettext("Health Centre")},
            {id: "dispensary", text: _.gettext("Dispensary")},
            {id: "clinic", text: _.gettext("Clinic")},
            {id: "mhsw", text: _.gettext("Ministry of Health and " +
                                         "Social Welfare")},
            {id: "council", text: _.gettext("Council")},
            {id: "training", text: _.gettext("Training Institution")},
            {id: "zonal-training", text: _.gettext("Zonal Training" +
                                                   " Centre")},
            {id: "ngo", text: _.gettext("NGO")}
        ]);
        return p;
    };

    self.list_facilities = function(district, facility_type, query) {
        var p = new Promise();
        p.callback([
            {id: "wazazi-galapo", text: "Wazazi Galapo"},
            {id: "wazazi-magugu", text: "Wazazi Magugu"},
            {id: "wazazu-mchuo", text: "Wazazu Mchuo"}
        ]);
        return p;
    };

    self.list_specialities = function(cadre_id) {
        var p = new Promise();
        var specialities = [];
        if (cadre_id == 67) {
            specialities = [
                {id: "cd", text: "Community Dentistry"},
                {id: "ms", text: "Maxilofacial Surgery"}
            ];
        }
        else if (cadre_id == 1) {
            specialities = [
                {id: "anaesthesia", text: "Anaesthesia"},
                {id: "anatomy", text: "Anatomy"}
            ];
        }
        else if (cadre_id == 60) {
            specialities = [
                {id: "anaesthesiology", text: "Anaesthesiology"},
                {id: "em", text: "Emergency Medicime"}
            ];
        }
        p.callback(specialities);
        return p;
    };

    self.submit_unknown_cadre = function(user_addr, cadre_name) {
        // Do nothing.
        return success();
    };

    self.submit_unknown_facility = function(user_addr, facility, region,
                                            facility_type) {
        // Do nothing.
        return success();
    };

    self.dob_to_obj = function(dob) {
        return {
            day: Number(dob.slice(0, 2)),
            month: Number(dob.slice(2, 4)),
            year: Number(dob.slice(4, 8))
        };
    };

    self.check_date_of_birth = function(user, dob) {
        return success(true);
    };

    self.cadre_needs_specialties = function(cadre_id) {
        var cadres_with_specialties = [1, 67, 60];
        var needs_specialties = cadres_with_specialties.some(
            function (i) {
                return i == cadre_id;
            });
        return success(needs_specialties);
    };

    self.register_health_worker = function(health_worker) {
        // Do nothing.
    };
}


function SwitchboardApiError(msg) {
    var self = this;
    self.msg = msg;

    self.toString = function() {
        return "<SwitchboardApiError: " + self.msg + ">";
    };
}


function SwitchboardApi(im, url, username, password) {

    var self = this;

    self.im = im;
    self.lang = im.user.lang || "en";
    self.url = url;
    self.headers = {
        'Content-Type': ['application/json']
    };

    if (username) {
        var hash = (new Buffer(username + ":" + password)).toString('base64');
        self.headers['Authorization'] = ['Basic ' + hash];
    }

    self.check_reply = function(reply, url, method, data, ignore_error) {
        var error;
        if (reply.success && reply.code == 200) {
            var json = JSON.parse(reply.body);
            if (json.status === 0) {
                return json;
            }
            error = ("API did not return status OK (got " +
                     json.status + " instead)");
        }
        else {
            error = reply.reason;
        }
        var error_msg = ("SwB API " + method + " to " + url + " failed: " +
                         error);
        if (typeof data != 'undefined') {
            error_msg = error_msg + '; data: ' + JSON.stringify(data);
        }
        self.im.log(error_msg);
        if (!ignore_error) {
            throw new SwitchboardApiError(error_msg);
        }
    };

    self.api_get = function(api_cmd, params) {
        var p = new Promise();
        var url = self.url + api_cmd;
        var items = [];
        for (var key in params) {
            items[items.length] = (encodeURIComponent(key) + '=' +
                                   encodeURIComponent(params[key]));
        }
        if (items.length !== 0) {
            url = url + '?' + items.join('&');
        }
        self.im.api.request("http.get", {
                url: url,
                headers: self.headers
            },
            function(reply) {
                var json = self.check_reply(reply, url, 'GET', false);
                p.callback(json);
            });
        return p;
    };

    self.api_post = function(api_cmd, data, ignore_error) {
        var p = new Promise();
        var url = self.url + api_cmd;
        self.im.api.request("http.post", {
                url: url,
                headers: self.headers,
                data: JSON.stringify(data)
            },
            function(reply) {
                var json = self.check_reply(reply, url, 'POST', data,
                                            ignore_error);
                p.callback(json);
            });
        return p;
    };

    // only allow printable ASCII x20 (space) to x73 (tilde)
    self.non_printable_ascii_re = /[^\x20-\x7E]/g;

    self.clean_title = function(title) {
        return title.replace(self.non_printable_ascii_re, '?');
    };

    self.list_cadres = function() {
        var p = self.api_get('specialties', {lang: self.lang});
        p.add_callback(function (result) {
            var cadres = result.specialties.filter(function (s) {
                return (s.parent_specialty_id === null);
            });
            return cadres.map(function (s) {
                var text = s.short_title ? s.short_title : s.title;
                text = self.clean_title(text);
                return {id: s.id, text: text};
            });
        });
        return p;
    };

    self.list_districts = function(query) {
        var p = self.api_get('regions', {
            type: 'District',
            title: query,
            lang: self.lang
        });
        p.add_callback(function (result) {
            return result.regions.map(function (r) {
                return {id: r.id, text: self.clean_title(r.title)};
            });
        });
        return p;
    };

    self.list_facility_types = function() {
        var p = self.api_get('facility-types', {lang: self.lang});
        p.add_callback(function (result) {
            return result.facility_types.map(function (f) {
                return {id: f.id, text: self.clean_title(f.title)};
            });
        });
        return p;
    };

    self.list_facilities = function(district, facility_type, query) {
        var params = {
            title: query,
            lang: self.lang
        };
        if (district !== null) {
            params.region = district;
        }
        if (facility_type !== null) {
            params.type = facility_type;
        }
        var p = self.api_get('facilities', params);
        p.add_callback(function (result) {
            return result.facilities.map(function (f) {
                return {id: f.id, text: self.clean_title(f.title)};
            });
        });
        return p;
    };

    self.list_specialities = function(cadre_id) {
        cadre_id = Number(cadre_id);
        var p = self.api_get('specialties', {lang: self.lang});
        p.add_callback(function (result) {
            var specialties = result.specialties.filter(function (s) {
                return (s.parent_specialty_id === cadre_id);
            });
            return specialties.map(function (s) {
                var text = s.short_title ? s.short_title : s.title;
                text = self.clean_title(text);
                return {id: s.id, text: text};
            });
        });
        return p;
    };

    self.submit_unknown_cadre = function(user_addr, cadre_name) {
        var p = self.api_post('specialties', {
            msisdn: user_addr.slice(0, 32), // maximum size allowed by API
            title: cadre_name,
            parent_specialty: null,
            lang: self.lang
        }, true); // TODO: stop ignoring errors once API accepts duplicates
        p.add_callback(function (result) {
            return (typeof result == "undefined") ? null : result.id;
        });
        return p;
    };

    self.submit_unknown_facility = function(user_addr, facility, region,
                                            facility_type) {
        var p = self.api_post('facilities', {
            msisdn: user_addr.slice(0, 32), // maximum size allowed by API
            title: facility,
            region: region,
            type: facility_type,
            address: null,
            lang: self.lang
        }, true); // TODO: stop ignoring errors once API accepts duplicates
        p.add_callback(function (result) {
            return (typeof result == "undefined") ? null : result.id;
        });
        return p;
    };

    self.dob_to_obj = function(dob) {
        return {
            day: Number(dob.slice(0, 2)),
            month: Number(dob.slice(2, 4)),
            year: Number(dob.slice(4, 8))
        };
    };

    self.obj_to_dob = function(obj) {
        var day = ("00" + obj.day.toString()).slice(-2);
        var month = ("00" + obj.month.toString()).slice(-2);
        var year = obj.year.toString();
        return day + month + year;
    };

    self._check_dob = function(dob, hw_birthdate) {
        if (!hw_birthdate) {
            return true;
        }
        return (dob == self.obj_to_dob(hw_birthdate));
    };

    self.check_date_of_birth = function(user, dob) {
        // TODO: currently disabled at SwB's request
        return success(true);
        var p;
        if (user.answers.registration_number &&
            user.answers.registration_number != '0') {
            // use mct-registrations for registration number
            p = self.api_get('mct-registrations', {
                registration: user.answers.registration_number
            });
            p.add_callback(function (result) {
                if (result.health_workers.length == 1) {
                    var hw = result.health_workers[0];
                    return self._check_dob(dob, hw.birthdate);
                }
                return false;
            });
        }
        else if (user.answers.cheque_number &&
                 user.answers.cheque_number != '0') {
            // use mct-payrolls for check number
            p = self.api_get('mct-payrolls', {
                check: user.answers.cheque_number
            });
            p.add_callback(function (result) {
                if (result.mct_payrolls.length == 1) {
                    var hw = result.mct_payrolls[0];
                    return self._check_dob(dob, hw.birthdate);
                }
                return false;
            });
        }
        else {
            p = success(true);
        }

        return p;
    };

    self.cadre_needs_specialties = function(cadre_id) {
        cadre_id = Number(cadre_id);
        var p = self.api_get('specialties', {lang: self.lang});
        p.add_callback(function (result) {
            var cadres = result.specialties.filter(function (s) {
                return (s.id === cadre_id);
            });
            if (cadres.length != 1) {
                return false;
            }
            var cadre = cadres[0];
            return cadre.is_query_subspecialties;
        });
        return p;
    };

    self.register_health_worker = function(health_worker) {
        var p = self.api_post('health-workers', {
            name: health_worker.full_name, // string (required)
            surname: health_worker.surname, // string (required)
            specialties: health_worker.specialties, // [SpecialtyID, ...]
            birthdate: health_worker.birthdate, // {day: 1, month: 2, ...}
            country: health_worker.country, // string
            email: health_worker.email, // string
            facility: health_worker.facility, // FacilityID, primary facility
            vodacom_phone: health_worker.vodacom_phone, // string, MSISDN
            mct_registration_number: health_worker.registration_number, // str
            mct_payroll_number: health_worker.cheque_number, // string
            // other_phone: health_worker.other_phone, // string, MSISDN
            // address: health_worker.address, // string, postal address
            language: self.lang
        });
        return p;
    };
}


function RegisterHealthWorker() {
    var self = this;
    StateCreator.call(self, "intro");

    var _ = new jed({});
    self.options_per_page = 5;
    self.characters_per_page = 163;

    // SwB API creator

    self.swb_api = function(im) {
        var cfg = im.config.swb_api;
        if (!cfg) {
            im.log("Using dummy Switchboard API.");
            return new DummySwitchboardApi(im);
        }
        im.log("Using real Switchboard API.");
        return new SwitchboardApi(im, cfg.url, cfg.username, cfg.password);
    };

    self.qa = function(im) {
        if (im.config.qa) {
            return true;
        }
        return false;
    };

    // SMSes

    self.send_sms = function(im, content) {
        var sms_tag = im.config.sms_tag;
        if (!sms_tag) return success(true);
        var p = new Promise();
        im.api.request("outbound.send_to_tag", {
            to_addr: im.user_addr,
            content: content,
            tagpool: sms_tag[0],
            tag: sms_tag[1]
        }, function(reply) {
            p.callback(reply.success);
        });
        return p;
    };

    self.send_sms_session1_abort = function(im) {
        var _ = im.i18n;
        var msg = _.gettext("If you would like to register at a later" +
                            " date please dial *149*24#.");
        return self.send_sms(im, msg);
    };

    self.send_sms_session1_end = function(im) {
        var _ = im.i18n;
        var msg = _.gettext("Thank you for beginning your registration" +
                            " process. Please dial *149*24# again to" +
                            " complete your registration in a few easy steps.");
        return self.send_sms(im, msg);
    };

    self.send_sms_session2_end = function(im) {
        var _ = im.i18n;
        var msg = _.gettext("Thank you for registering with The Health Network" +
                            " Programme. We will process your registration and" +
                            " confirm with you when you are registered.");
        return self.send_sms(im, msg);
    };

    self.send_sms_first_possible_timeout = function(im) {
        var _ = im.i18n;
        var msg = _.gettext("Your session has ended but you have not" +
                            " completed your registration. Please dial" +
                            " *149*24# again to continue with your" +
                            " registration where you left off.");
        return self.send_sms(im, msg);
    };

    // Vodacom number checker

    self.check_from_addr = function(im) {
        var patterns = im.config.valid_user_addresses;
        if (!patterns ||
            typeof patterns.length == 'undefined' ||
            patterns.length === 0) {
            return true;
        }
        var okay = patterns.some(function (p) {
            return Boolean(im.user_addr.match(p));
        });
        return okay;
    };

    // Session handling

    self.get_user_item = function(user, item, default_value) {
        var custom = user.custom || {};
        var value = custom[item];
        return (typeof value != 'undefined') ? value : default_value;
    };

    self.set_user_item = function(user, item, value) {
        if (typeof user.custom == 'undefined') {
            user.custom = {};
        }
        user.custom[item] = value;
    };

    self.inc_user_item = function(user, item) {
        var value = self.get_user_item(user, item, 0) + 1;
        self.set_user_item(user, item, value);
        return value;
    };

    // IM event callbacks

    self.on_session_new = function(event) {
        event.im.metrics.fire_inc('ussd_sessions');
        event.im.metrics.fire_inc('session_new_in.' +
                                  event.im.current_state.name);
        self.inc_user_item(event.im.user, 'ussd_sessions');
    };

    self.on_session_close = function(event) {
        event.im.metrics.fire_inc('session_closed_in.' +
                                  event.im.current_state.name);
        if (event.data.possible_timeout) {
            event.im.metrics.fire_inc('possible_timeout_in.' +
                                      event.im.current_state.name);
            var timeouts = self.inc_user_item(event.im.user,
                                              'possible_timeouts');
            if (timeouts <= 1) {
                self.send_sms_first_possible_timeout(event.im);
            }
        }
    };

    self.on_new_user = function(event) {
        event.im.metrics.fire_inc('unique_users');
    };

    self.on_state_enter = function(event) {
        event.im.metrics.fire_inc('state_entered.' + event.data.state.name);
    };

    self.on_state_exit = function(event) {
        event.im.metrics.fire_inc('state_exited.' + event.data.state.name);
    };

    // Session 1

        self.add_state(new LanguageChoice(
            "intro",
            function (choice) {
                if (self.check_from_addr(this.im)) {
                    return "cadre";
                }
                return "no_vodacom_sim";
            },
            _.gettext("Welcome to the Health Network brought to you by" +
                      " Switchboard, the Ministry of Health and Vodacom." +
                      "\n\n" +
                      "What language would you like to use?"),
            [
                new Choice("sw", "Swahili"),
                new Choice("en", "English")
            ],
            _.gettext("Please select a valid language.")
        ));
        self.add_state(new EndState(
            "no_vodacom_sim",
            _.gettext("Sorry. This service is only available to Health" +
                      " Practitioners with a Vodacom Sim card. Please" +
                      " register a new Vodacom SIM and then redial this" +
                      " number."),
            "intro"
        ));
        self.add_creator("cadre", function(state_name, im) {
            var _ = im.i18n;
            var swb_api = self.swb_api(im);
            var p = swb_api.list_cadres();
            p.add_callback(function (cadres) {
                var choices = cadres.map(function (c) {
                    return new Choice(c.id, c.text);
                });
                choices[choices.length] = new Choice("other",
                                                     _.gettext("Other"));
                return new PaginatedChoiceState(
                    state_name,
                    function (choice) {
                        return (choice.value == "other" ?
                                "cadre_other" :
                                "cheque_number");
                    },
                    _.gettext("What CADRE are you?"),
                    choices, null, {},
                    {
                        more: im.i18n.gettext("View more"),
                        back: im.i18n.gettext("Back"),
                        options_per_page: self.options_per_page,
                        characters_per_page: self.characters_per_page
                    }
                );
            });
            return p;
        });
        self.add_state(new FreeText(
            "cadre_other",
            "cadre_unavailable",
            _.gettext("Please specify your CADRE:")
        ));
        self.add_state(new ChoiceState(
            "cadre_unavailable",
            function (choice, done) {
                if (choice.value == "yes") {
                    var im = this.im;
                    var swb_api = self.swb_api(im);
                    var cadre_name = im.get_user_answer("cadre_other");
                    var p = swb_api.submit_unknown_cadre(im.user_addr,
                                                         cadre_name);
                    p.add_callback(function (result) {
                        done("cadre_unavailable_contact");
                    });
                }
                else {
                    done("cadre_unavailable_dont_contact");
                }
            },
            _.gettext("Sorry but this service is not yet available for" +
                      " your CADRE. Would you like us to contact you if this" +
                      " service becomes available for you in the future?"),
            [
                new Choice("yes", _.gettext("Yes")),
                new Choice("no", _.gettext("No"))
            ]
        ));
        self.add_state(new EndState(
            "cadre_unavailable_contact",
            _.gettext("Thank you for trying to register, we will contact" +
                      " you when the programme is available for your cadre."),
            "intro"
        ));
        self.add_state(new EndState(
            "cadre_unavailable_dont_contact",
            _.gettext("Thank you for trying to register."),
            "intro"
        ));
        self.add_state(new EndState(
            "end", // to support old users who ended sessions in this state
            _.gettext("Thank you for trying to register."),
            "intro"
        ));
        self.add_state(new FreeText(
            "cheque_number",
            function (content) {
                return (content == '0' ?
                        // "registration_number" : "date_of_birth");
                        "registration_number" : "first_name");
            },
            _.gettext("If u R a government Health Worker please enter the" +
                      " last 7-9 digits of Ur cheque no (eg 1234567)." +
                      " If U don't have a cheque no enter '0'"),
            function (content) {
                return Boolean(content.match("^([0-9]{7,9}|[0Oo])$"));
            },
            _.gettext("Sorry but that is not a valid cheque number." +
                      " Please try again of enter '0' if you do not" +
                      " have a cheque number.")
        ));
        self.add_state(new FreeText(
            "registration_number",
            // "date_of_birth",
            "first_name",
            _.gettext("To verify that u r a registered health worker enter" +
                      " your REGISTRATION No (eg 1234) from Medical" +
                      " Council of Tanganyika or enter '0' if u do not have" +
                      " a reg no"),
            function (content) {
                return Boolean(content.match("^([0-9]{1,5}|[0Oo])$"));
            },
            _.gettext("Sorry but that is not a valid Registration Number." +
                      " Please try again or enter 0 if you do not have a" +
                      " registration number.")
        ));
        // Note: Date of birth state disabled at SwB's request
        self.add_state(new FreeText(
            "date_of_birth",
            function (content, done) {
                var swb_api = self.swb_api(this.im);
                var p = swb_api.check_date_of_birth(this.im.user, content);
                p.add_callback(function (result) {
                    if (result) {
                        done("first_name");
                    }
                    else {
                        done("dont_match_mct");
                    }
                });
            },
            _.gettext("Please enter your date of birth in the following" +
                      " format DDMMYYYY (eg if born on 23 April 1980," +
                      " enter 23041980)."),
            function (content) {
                if (!content.match("^[0-9]{8}$"))
                    return false;
                var day = Number(content.slice(0, 2));
                var month = Number(content.slice(2, 4));
                var year = Number(content.slice(4, 8));
                var year_now = (new Date()).getFullYear();
                if (day > 31 || month > 12 || year_now - year < 16)
                    return false;
                return true;
            },
            _.gettext("Sorry but you have not entered a valid date." +
                      " Please enter your date of birth in the following" +
                      " format DDMMYYYY (e.g. if born on 23 April 1980," +
                      " enter 23041980).")
        ));
        self.add_state(new ChoiceState(
            "dont_match_mct",
            function (choice) {
                return (choice.value == "again" ?
                        "cheque_number" :
                        "dont_match_mct_end");
            },
            _.gettext("Sorry but the registration number and DOB that you" +
                      " have entered do not match with MCT records."),
            [
                new Choice("again", _.gettext("Enter details again")),
                new Choice("end", _.gettext("End Session"))
            ]
        ));
        self.add_state(new EndState(
            "dont_match_mct_end",
            _.gettext("Sorry but we cannot verify you at this point. " +
                      "Please verify your details with MCT and dial " +
                      "*149*24# again to register."),
            "intro"
        ));
        self.add_state(new FreeText(
            "first_name",
            "surname",
            _.gettext("Please enter your first name.")
        ));
        self.add_state(new FreeText(
            "surname",
            "terms_and_conditions",
            _.gettext("Please enter your surname.")
        ));
        self.add_state(new ChoiceState(
            "terms_and_conditions",
            function (choice) {
                return (choice.value == "yes" ?
                        "session1_end" :
                        "session1_abort_yn");
            },
            _.gettext("Do you agree to the terms and conditions as laid" +
                      " out at http://www.healthnetwork.or.tz ?" +
                      " Your local DMO will also have a copy."),
            [
                new Choice("yes", _.gettext("Yes")),
                new Choice("no", _.gettext("No"))
            ]
        ));
        self.add_state(new ChoiceState(
            "session1_abort_yn",
            function (choice) {
                return (choice.value == "yes" ?
                        "session1_abort" :
                        "terms_and_conditions");
            },
            _.gettext("We are sorry but you cannot be registered unless" +
                      " you agree to the terms and conditions. Are you" +
                      " sure you would like to end the registration process?"),
            [
                new Choice("yes", _.gettext("Yes")),
                new Choice("no", _.gettext("No"))
            ]
        ));
        self.add_state(new EndState(
            "session1_abort",
            _.gettext("If you would like to register at a later date" +
                      " please dial *149*24#."),
            "intro",
            {
                on_enter: function () {
                    return self.send_sms_session1_abort(this.im);
                }
            }
        ));
        self.add_state(new EndState(
            "session1_end",
            _.gettext("Thank you. You have almost completed your " +
                      "registration process. Please dial *149*24# " +
                      "again to complete just a few more questions."),
            "session2_intro",
            {
                on_enter: function () {
                    this.im.metrics.fire_inc('first_session_completed');
                    return self.send_sms_session1_end(this.im);
                }
            }
        ));

    // Session 2

        self.facility_or_district_select = function (content, done) {
            var swb_api = self.swb_api(this.im);
            var im = this.im;
            var p = swb_api.list_districts(content);
            p.add_callback(function (districts) {
                if (districts.length == 1) {
                    im.set_user_answer("district_select",
                                       districts[0].id);
                    done("facility_type");
                }
                else {
                    done("district_select");
                }
            });
        };

        self.add_state(new FreeText(
            "session2_intro",
            self.facility_or_district_select,
            _.gettext("Welcome back 2 the Health Network brought 2 U by " +
                      "Switchboard, the Ministry of Health and Vodacom." +
                      "\n\n" +
                      "Please enter your district (eg Kilosa). No" +
                      " abbreviations please."),
            null, null, {
                on_enter: function () {
                    // clear district_reenter value on state enter
                    // in case a user gets to re-use the menu
                    this.im.set_user_answer("district_reenter", null);
                }
            }
        ));
        self.add_creator("district_select", function(state_name, im) {
            var _ = im.i18n;
            var swb_api = self.swb_api(im);
            var first_query = im.get_user_answer("session2_intro");
            var second_query = im.get_user_answer("district_reenter");
            var query = second_query || first_query;
            var p = swb_api.list_districts(query);
            p.add_callback(function (districts) {
                var choices = districts.map(function (d) {
                    return new Choice(d.id, d.text);
                });
                choices[choices.length] = new Choice(
                    null, _.gettext("None of the above"));

                return new PaginatedChoiceState(
                    state_name,
                    function (choice) {
                        if (choice.value === null && !second_query) {
                            return "district_reenter";
                        }
                        return "facility_type";
                    },
                    _.gettext("The district you entered cannot be found." +
                              " Did you mean:"),
                    choices, null, {},
                    {
                        more: im.i18n.gettext("View more"),
                        back: im.i18n.gettext("Back"),
                        options_per_page: self.options_per_page,
                        characters_per_page: self.characters_per_page
                    }
                );
            });
            return p;
        });
        self.add_state(new FreeText(
            "district_reenter",
            self.facility_or_district_select,
            _.gettext("Please re-enter your district:")
        ));
        self.add_creator("facility_type", function(state_name, im) {
            var _ = im.i18n;
            var swb_api = self.swb_api(im);
            var p = swb_api.list_facility_types();
            p.add_callback(function (facility_types) {
                var choices = facility_types.map(function (f) {
                    return new Choice(f.id, f.text);
                });
                choices[choices.length] = new Choice(null,
                                                     _.gettext("Other"));
                return new PaginatedChoiceState(
                    state_name,
                    "facility_name",
                    _.gettext("Please enter your facility type:"),
                    choices, null, {},
                    {
                        more: im.i18n.gettext("View more"),
                        back: im.i18n.gettext("Back"),
                        options_per_page: self.options_per_page,
                        characters_per_page: self.characters_per_page
                    }
                );
            });
            return p;
        });
        self.add_state(new FreeText(
            "facility_name",
            function (content, done) {
                var im = this.im;
                var swb_api = self.swb_api(im);
                var district = im.get_user_answer("district_select");
                var facility_type = im.get_user_answer("facility_type");
                var query = content;
                var p = swb_api.list_facilities(district, facility_type, query);
                p.add_callback(function (facilities) {
                    var ev_p = success();
                    if (facilities.length == 1) {
                        im.set_user_answer("facility_select", facilities[0].id);
                        ev_p.add_callback(function (result) {
                            var cadre_id = im.get_user_answer("cadre");
                            return swb_api.cadre_needs_specialties(cadre_id);
                        });
                        p.add_callback(function (needs_specialties) {
                            if (needs_specialties) {
                                done("select_speciality");
                            }
                            else {
                                // done("email");
                                done("session2_end");
                            }
                        });
                    }
                    else {
                        ev_p.add_callback(function (result) {
                            done("facility_select");
                        });
                    }
                    return ev_p;
                });
            },
            _.gettext("Please enter the official name of the facility where" +
                      " you primarily practice")
        ));
        self.add_creator("facility_select", function (state_name, im) {
            var _ = im.i18n;
            var swb_api = self.swb_api(im);
            var district = im.get_user_answer("district_select");
            var facility_type = im.get_user_answer("facility_type");
            var query = im.get_user_answer("facility_name");
            var p = swb_api.list_facilities(district, facility_type, query);
            p.add_callback(function (facilities) {
                var choices = facilities.map(function (f) {
                    return new Choice(f.id, f.text);
                });
                choices[choices.length] = new Choice(
                    null, im.i18n.gettext("None of the above"));

                return new PaginatedChoiceState(
                    state_name,
                    function (choice, done) {
                        var p = success();
                        if (choice.value === null) {
                            p.add_callback(function (result) {
                                return swb_api.submit_unknown_facility(
                                    im.user_addr, query, district,
                                    facility_type);
                            });
                        }
                        p.add_callback(function (result) {
                            var cadre_id = im.get_user_answer("cadre");
                            return swb_api.cadre_needs_specialties(cadre_id);
                        });
                        p.add_callback(function (needs_specialties) {
                            if (needs_specialties) {
                                done("select_speciality");
                            }
                            else {
                                // done("email");
                                done("session2_end");
                            }
                        });
                    },
                    _.gettext("The Facility you entered cannot be found." +
                              " Did you mean:"),
                    choices, null, {},
                    {
                        more: im.i18n.gettext("View more"),
                        back: im.i18n.gettext("Back"),
                        options_per_page: self.options_per_page,
                        characters_per_page: self.characters_per_page
                    }
                );
            });
            return p;
        });
        self.add_creator("select_speciality", function (state_name, im) {
            var _ = im.i18n;
            var swb_api = self.swb_api(im);
            var cadre_id = im.get_user_answer("cadre");
            var p = swb_api.list_specialities(cadre_id);
            p.add_callback(function (specialities) {
                var choices = specialities.map(function (s) {
                    return new Choice(s.id, s.text);
                });
                choices[choices.length] = new Choice(null,
                                                     _.gettext("Other"));
                return new PaginatedChoiceState(
                    state_name,
                    // "email",
                    "session2_end",
                    _.gettext("Please enter your specialty:"),
                    choices, null, {},
                    {
                        more: im.i18n.gettext("View more"),
                        back: im.i18n.gettext("Back"),
                        options_per_page: self.options_per_page,
                        characters_per_page: self.characters_per_page
                    }
                );
            });
            return p;
        });
        // Note: Email state disabled at SwB's request
        self.add_state(new FreeText(
            "email",
            "session2_end",
            _.gettext("Please enter your email address or 0 if you do not " +
                      "have an email address"),
            function (content) {
                return Boolean(content.match("^(.+\\@.+\\..+|[0Oo])$"));
            },
            _.gettext("Sorry but that is not a valid email address. Please" +
                      " try again or enter 0 if you do not have an email" +
                      " address.")
        ));
        // Note: confirmation SMSes need to be sent manually via Vumi Go
        //       once the registration has been approved and activated.

        self.create_health_worker = function (im) {
            var ans = im.get_user_answer;
            var swb_api = self.swb_api(im);
            var hw = {};

            hw.full_name = ans("first_name") + " " + ans("surname");
            hw.surname = ans("surname");
            hw.specialties = [ans("cadre")];
            var specialty = ans("select_speciality");
            if (specialty)
                hw.specialties[hw.specialties.length] = specialty;
            var cheque_number = ans("cheque_number");
            if (cheque_number && !cheque_number.match('^[0Oo]$'))
                hw.cheque_number = cheque_number;
            var registration_number = ans("registration_number");
            if (registration_number && !registration_number.match('^[0Oo]$'))
                hw.registration_number = registration_number;
            var date_of_birth = ans("date_of_birth");
            if (date_of_birth)
                hw.birthdate = swb_api.dob_to_obj(date_of_birth);
            hw.country = "TZ";
            var email = ans("email");
            hw.email = (email && !email.match('^[0Oo]$')) ? email : null;
            hw.facility = ans("facility_select");
            hw.vodacom_phone = im.user_addr;

            return hw;
        };

        self.add_state(new EndState(
            "session2_end",
            _.gettext("Thank you for registering with The Health Network" +
                      " Programme. We will process your registration and" +
                      " confirm with you when you are registered."),
            function (content) {
                if (self.qa(this.im)) {
                    return "intro";
                }
                return "session2_end";
            },
            {
                on_enter: function () {
                    var im = this.im;
                    var swb_api = self.swb_api(im);
                    var health_worker = self.create_health_worker(im);
                    swb_api.register_health_worker(health_worker);
                    self.set_user_item(im.user, "registered", 1);
                    self.send_sms_session2_end(im);

                    im.metrics.fire_inc('second_session_completed');
                    var sessions = self.get_user_item(im.user,
                                                      'ussd_sessions', 0);
                    im.metrics.fire_avg('sessions_taken_to_register',
                                        sessions);
                }
            }
        ));

}


// launch app
var states = new RegisterHealthWorker();
var im = new InteractionMachine(api, states);<commit_msg>Make first_session_completed and second_session_completed absolute metrics.<commit_after>// Possible configuration items:
//
// * translation.<lang>:
//     Jed translation JSON for language (e.g. sw). Optional. If ommitted,
//     untranslated strings are used.
//
// * config:
//     * qa:
//         Set to true to turn on QA features. Default is false.
//     * swb_api:
//         Dictionary of username, password and url of the Switchboard API.
//         If ommitted, the dummy API is used instead.
//     * sms_tag:
//         Two element list of [pool, tag] giving the Go endpoint to send SMSes
//         out via. If ommitted, SMSes are not sent.
//     * metric_store:
//         Name of the metric store to use. If omitted, metrics are sent
//         to the metric store named 'default'.
//     * valid_user_addresses:
//         JSON list of allowed from_addr regular expression patterns.
//         Optional. If omitted, all from_addr values are allowed.
//
// It is suspected that the Vodacom Tz prefixes are: 255743 - 6, 25575 and
// 25576.
//
// Metrics produced:
//
// * ussd_sessions
// * unique_users
// * first_session_completed
// * second_session_completed
// * sessions_taken_to_register (average)
// * session_new_in.<state-name>
// * session_closed_in.<state-name>
// * possible_timeout_in.<state-name>
// * state_entered.<state-name>
// * state_exited.<state-name>

var vumigo = require("vumigo_v01");
var jed = require("jed");

if (typeof api === "undefined") {
    // testing hook (supplies api when it is not passed in by the real sandbox)
    var api = this.api = new vumigo.dummy_api.DummyApi();
}

var Promise = vumigo.promise.Promise;
var success = vumigo.promise.success;
var maybe_promise = vumigo.promise.maybe_promise;
var State = vumigo.states.State;
var Choice = vumigo.states.Choice;
var ChoiceState = vumigo.states.ChoiceState;
var LanguageChoice = vumigo.states.LanguageChoice;
var PaginatedChoiceState = vumigo.states.PaginatedChoiceState;
var FreeText = vumigo.states.FreeText;
var EndState = vumigo.states.EndState;
var InteractionMachine = vumigo.state_machine.InteractionMachine;
var StateCreator = vumigo.state_machine.StateCreator;


function DummySwitchboardApi(im) {

    var self = this;

    self.im = im;

    self.list_cadres = function() {
        var p = new Promise();
        p.callback([
            // Medical Specialist, AMO and Dental Specialist
            // use numerical IDs to match default IDs for cadres
            // with specialties.
            {id: 1, text: "Medical Specialist"},
            {id: "mo", text: "MO"}, // Medical Officer
            {id: 60, text: "AMO"}, // Assistant Medical Officer
            {id: "co", text: "CO"}, // Clinical Officer
            {id: "aco", text: "ACO"}, // Assistant Clinical Officer
            {id: 67, text: "Dental Specialist"},
            {id: "do", text: "Dental Officer"},
            {id: "ado", text: "ADO"}, // Assitant Dental Officer
            {id: "dt", text: "Dental Therapist"}
        ]);
        return p;
    };

    self.list_districts = function(query) {
        var p = new Promise();
        p.callback([
            {id: "kigoma-mc", text: "Kigoma MC"},
            {id: "kigoma-dc", text: "Kigoma DC"},
            {id: "kasulu-dc", text: "Kasulu DC"}
        ]);
        return p;
    };

    self.list_facility_types = function() {
        var p = new Promise();
        var _ = self.im.i18n;
        p.callback([
            {id: "hospital", text: _.gettext("Hospital")},
            {id: "health-centre", text: _.gettext("Health Centre")},
            {id: "dispensary", text: _.gettext("Dispensary")},
            {id: "clinic", text: _.gettext("Clinic")},
            {id: "mhsw", text: _.gettext("Ministry of Health and " +
                                         "Social Welfare")},
            {id: "council", text: _.gettext("Council")},
            {id: "training", text: _.gettext("Training Institution")},
            {id: "zonal-training", text: _.gettext("Zonal Training" +
                                                   " Centre")},
            {id: "ngo", text: _.gettext("NGO")}
        ]);
        return p;
    };

    self.list_facilities = function(district, facility_type, query) {
        var p = new Promise();
        p.callback([
            {id: "wazazi-galapo", text: "Wazazi Galapo"},
            {id: "wazazi-magugu", text: "Wazazi Magugu"},
            {id: "wazazu-mchuo", text: "Wazazu Mchuo"}
        ]);
        return p;
    };

    self.list_specialities = function(cadre_id) {
        var p = new Promise();
        var specialities = [];
        if (cadre_id == 67) {
            specialities = [
                {id: "cd", text: "Community Dentistry"},
                {id: "ms", text: "Maxilofacial Surgery"}
            ];
        }
        else if (cadre_id == 1) {
            specialities = [
                {id: "anaesthesia", text: "Anaesthesia"},
                {id: "anatomy", text: "Anatomy"}
            ];
        }
        else if (cadre_id == 60) {
            specialities = [
                {id: "anaesthesiology", text: "Anaesthesiology"},
                {id: "em", text: "Emergency Medicime"}
            ];
        }
        p.callback(specialities);
        return p;
    };

    self.submit_unknown_cadre = function(user_addr, cadre_name) {
        // Do nothing.
        return success();
    };

    self.submit_unknown_facility = function(user_addr, facility, region,
                                            facility_type) {
        // Do nothing.
        return success();
    };

    self.dob_to_obj = function(dob) {
        return {
            day: Number(dob.slice(0, 2)),
            month: Number(dob.slice(2, 4)),
            year: Number(dob.slice(4, 8))
        };
    };

    self.check_date_of_birth = function(user, dob) {
        return success(true);
    };

    self.cadre_needs_specialties = function(cadre_id) {
        var cadres_with_specialties = [1, 67, 60];
        var needs_specialties = cadres_with_specialties.some(
            function (i) {
                return i == cadre_id;
            });
        return success(needs_specialties);
    };

    self.register_health_worker = function(health_worker) {
        // Do nothing.
    };
}


function SwitchboardApiError(msg) {
    var self = this;
    self.msg = msg;

    self.toString = function() {
        return "<SwitchboardApiError: " + self.msg + ">";
    };
}


function SwitchboardApi(im, url, username, password) {

    var self = this;

    self.im = im;
    self.lang = im.user.lang || "en";
    self.url = url;
    self.headers = {
        'Content-Type': ['application/json']
    };

    if (username) {
        var hash = (new Buffer(username + ":" + password)).toString('base64');
        self.headers['Authorization'] = ['Basic ' + hash];
    }

    self.check_reply = function(reply, url, method, data, ignore_error) {
        var error;
        if (reply.success && reply.code == 200) {
            var json = JSON.parse(reply.body);
            if (json.status === 0) {
                return json;
            }
            error = ("API did not return status OK (got " +
                     json.status + " instead)");
        }
        else {
            error = reply.reason;
        }
        var error_msg = ("SwB API " + method + " to " + url + " failed: " +
                         error);
        if (typeof data != 'undefined') {
            error_msg = error_msg + '; data: ' + JSON.stringify(data);
        }
        self.im.log(error_msg);
        if (!ignore_error) {
            throw new SwitchboardApiError(error_msg);
        }
    };

    self.api_get = function(api_cmd, params) {
        var p = new Promise();
        var url = self.url + api_cmd;
        var items = [];
        for (var key in params) {
            items[items.length] = (encodeURIComponent(key) + '=' +
                                   encodeURIComponent(params[key]));
        }
        if (items.length !== 0) {
            url = url + '?' + items.join('&');
        }
        self.im.api.request("http.get", {
                url: url,
                headers: self.headers
            },
            function(reply) {
                var json = self.check_reply(reply, url, 'GET', false);
                p.callback(json);
            });
        return p;
    };

    self.api_post = function(api_cmd, data, ignore_error) {
        var p = new Promise();
        var url = self.url + api_cmd;
        self.im.api.request("http.post", {
                url: url,
                headers: self.headers,
                data: JSON.stringify(data)
            },
            function(reply) {
                var json = self.check_reply(reply, url, 'POST', data,
                                            ignore_error);
                p.callback(json);
            });
        return p;
    };

    // only allow printable ASCII x20 (space) to x73 (tilde)
    self.non_printable_ascii_re = /[^\x20-\x7E]/g;

    self.clean_title = function(title) {
        return title.replace(self.non_printable_ascii_re, '?');
    };

    self.list_cadres = function() {
        var p = self.api_get('specialties', {lang: self.lang});
        p.add_callback(function (result) {
            var cadres = result.specialties.filter(function (s) {
                return (s.parent_specialty_id === null);
            });
            return cadres.map(function (s) {
                var text = s.short_title ? s.short_title : s.title;
                text = self.clean_title(text);
                return {id: s.id, text: text};
            });
        });
        return p;
    };

    self.list_districts = function(query) {
        var p = self.api_get('regions', {
            type: 'District',
            title: query,
            lang: self.lang
        });
        p.add_callback(function (result) {
            return result.regions.map(function (r) {
                return {id: r.id, text: self.clean_title(r.title)};
            });
        });
        return p;
    };

    self.list_facility_types = function() {
        var p = self.api_get('facility-types', {lang: self.lang});
        p.add_callback(function (result) {
            return result.facility_types.map(function (f) {
                return {id: f.id, text: self.clean_title(f.title)};
            });
        });
        return p;
    };

    self.list_facilities = function(district, facility_type, query) {
        var params = {
            title: query,
            lang: self.lang
        };
        if (district !== null) {
            params.region = district;
        }
        if (facility_type !== null) {
            params.type = facility_type;
        }
        var p = self.api_get('facilities', params);
        p.add_callback(function (result) {
            return result.facilities.map(function (f) {
                return {id: f.id, text: self.clean_title(f.title)};
            });
        });
        return p;
    };

    self.list_specialities = function(cadre_id) {
        cadre_id = Number(cadre_id);
        var p = self.api_get('specialties', {lang: self.lang});
        p.add_callback(function (result) {
            var specialties = result.specialties.filter(function (s) {
                return (s.parent_specialty_id === cadre_id);
            });
            return specialties.map(function (s) {
                var text = s.short_title ? s.short_title : s.title;
                text = self.clean_title(text);
                return {id: s.id, text: text};
            });
        });
        return p;
    };

    self.submit_unknown_cadre = function(user_addr, cadre_name) {
        var p = self.api_post('specialties', {
            msisdn: user_addr.slice(0, 32), // maximum size allowed by API
            title: cadre_name,
            parent_specialty: null,
            lang: self.lang
        }, true); // TODO: stop ignoring errors once API accepts duplicates
        p.add_callback(function (result) {
            return (typeof result == "undefined") ? null : result.id;
        });
        return p;
    };

    self.submit_unknown_facility = function(user_addr, facility, region,
                                            facility_type) {
        var p = self.api_post('facilities', {
            msisdn: user_addr.slice(0, 32), // maximum size allowed by API
            title: facility,
            region: region,
            type: facility_type,
            address: null,
            lang: self.lang
        }, true); // TODO: stop ignoring errors once API accepts duplicates
        p.add_callback(function (result) {
            return (typeof result == "undefined") ? null : result.id;
        });
        return p;
    };

    self.dob_to_obj = function(dob) {
        return {
            day: Number(dob.slice(0, 2)),
            month: Number(dob.slice(2, 4)),
            year: Number(dob.slice(4, 8))
        };
    };

    self.obj_to_dob = function(obj) {
        var day = ("00" + obj.day.toString()).slice(-2);
        var month = ("00" + obj.month.toString()).slice(-2);
        var year = obj.year.toString();
        return day + month + year;
    };

    self._check_dob = function(dob, hw_birthdate) {
        if (!hw_birthdate) {
            return true;
        }
        return (dob == self.obj_to_dob(hw_birthdate));
    };

    self.check_date_of_birth = function(user, dob) {
        // TODO: currently disabled at SwB's request
        return success(true);
        var p;
        if (user.answers.registration_number &&
            user.answers.registration_number != '0') {
            // use mct-registrations for registration number
            p = self.api_get('mct-registrations', {
                registration: user.answers.registration_number
            });
            p.add_callback(function (result) {
                if (result.health_workers.length == 1) {
                    var hw = result.health_workers[0];
                    return self._check_dob(dob, hw.birthdate);
                }
                return false;
            });
        }
        else if (user.answers.cheque_number &&
                 user.answers.cheque_number != '0') {
            // use mct-payrolls for check number
            p = self.api_get('mct-payrolls', {
                check: user.answers.cheque_number
            });
            p.add_callback(function (result) {
                if (result.mct_payrolls.length == 1) {
                    var hw = result.mct_payrolls[0];
                    return self._check_dob(dob, hw.birthdate);
                }
                return false;
            });
        }
        else {
            p = success(true);
        }

        return p;
    };

    self.cadre_needs_specialties = function(cadre_id) {
        cadre_id = Number(cadre_id);
        var p = self.api_get('specialties', {lang: self.lang});
        p.add_callback(function (result) {
            var cadres = result.specialties.filter(function (s) {
                return (s.id === cadre_id);
            });
            if (cadres.length != 1) {
                return false;
            }
            var cadre = cadres[0];
            return cadre.is_query_subspecialties;
        });
        return p;
    };

    self.register_health_worker = function(health_worker) {
        var p = self.api_post('health-workers', {
            name: health_worker.full_name, // string (required)
            surname: health_worker.surname, // string (required)
            specialties: health_worker.specialties, // [SpecialtyID, ...]
            birthdate: health_worker.birthdate, // {day: 1, month: 2, ...}
            country: health_worker.country, // string
            email: health_worker.email, // string
            facility: health_worker.facility, // FacilityID, primary facility
            vodacom_phone: health_worker.vodacom_phone, // string, MSISDN
            mct_registration_number: health_worker.registration_number, // str
            mct_payroll_number: health_worker.cheque_number, // string
            // other_phone: health_worker.other_phone, // string, MSISDN
            // address: health_worker.address, // string, postal address
            language: self.lang
        });
        return p;
    };
}


function RegisterHealthWorker() {
    var self = this;
    StateCreator.call(self, "intro");

    var _ = new jed({});
    self.options_per_page = 5;
    self.characters_per_page = 163;

    // SwB API creator

    self.swb_api = function(im) {
        var cfg = im.config.swb_api;
        if (!cfg) {
            im.log("Using dummy Switchboard API.");
            return new DummySwitchboardApi(im);
        }
        im.log("Using real Switchboard API.");
        return new SwitchboardApi(im, cfg.url, cfg.username, cfg.password);
    };

    self.qa = function(im) {
        if (im.config.qa) {
            return true;
        }
        return false;
    };

    // Session metrics helper


    self.incr_metric = function(im, metric) {
        var p = new Promise();
        p.add_callback(function (value) {
            im.metrics.fire_max('first_session_completed', value);
        });
        im.api.request(
            "kv.incr", {key: "metrics." + metric, amount: 1},
            function(reply) {
                if (reply.success) {
                    p.callback(reply.value);
                }
                else {
                    im.log("Failed to increment metric " + metric + ": " +
                           reply.reason);
                    p.callback(0);
                }
            });
        return p;
    }

    // SMSes

    self.send_sms = function(im, content) {
        var sms_tag = im.config.sms_tag;
        if (!sms_tag) return success(true);
        var p = new Promise();
        im.api.request("outbound.send_to_tag", {
            to_addr: im.user_addr,
            content: content,
            tagpool: sms_tag[0],
            tag: sms_tag[1]
        }, function(reply) {
            p.callback(reply.success);
        });
        return p;
    };

    self.send_sms_session1_abort = function(im) {
        var _ = im.i18n;
        var msg = _.gettext("If you would like to register at a later" +
                            " date please dial *149*24#.");
        return self.send_sms(im, msg);
    };

    self.send_sms_session1_end = function(im) {
        var _ = im.i18n;
        var msg = _.gettext("Thank you for beginning your registration" +
                            " process. Please dial *149*24# again to" +
                            " complete your registration in a few easy steps.");
        return self.send_sms(im, msg);
    };

    self.send_sms_session2_end = function(im) {
        var _ = im.i18n;
        var msg = _.gettext("Thank you for registering with The Health Network" +
                            " Programme. We will process your registration and" +
                            " confirm with you when you are registered.");
        return self.send_sms(im, msg);
    };

    self.send_sms_first_possible_timeout = function(im) {
        var _ = im.i18n;
        var msg = _.gettext("Your session has ended but you have not" +
                            " completed your registration. Please dial" +
                            " *149*24# again to continue with your" +
                            " registration where you left off.");
        return self.send_sms(im, msg);
    };

    // Vodacom number checker

    self.check_from_addr = function(im) {
        var patterns = im.config.valid_user_addresses;
        if (!patterns ||
            typeof patterns.length == 'undefined' ||
            patterns.length === 0) {
            return true;
        }
        var okay = patterns.some(function (p) {
            return Boolean(im.user_addr.match(p));
        });
        return okay;
    };

    // Session handling

    self.get_user_item = function(user, item, default_value) {
        var custom = user.custom || {};
        var value = custom[item];
        return (typeof value != 'undefined') ? value : default_value;
    };

    self.set_user_item = function(user, item, value) {
        if (typeof user.custom == 'undefined') {
            user.custom = {};
        }
        user.custom[item] = value;
    };

    self.inc_user_item = function(user, item) {
        var value = self.get_user_item(user, item, 0) + 1;
        self.set_user_item(user, item, value);
        return value;
    };

    // IM event callbacks

    self.on_session_new = function(event) {
        event.im.metrics.fire_inc('ussd_sessions');
        event.im.metrics.fire_inc('session_new_in.' +
                                  event.im.current_state.name);
        self.inc_user_item(event.im.user, 'ussd_sessions');
    };

    self.on_session_close = function(event) {
        event.im.metrics.fire_inc('session_closed_in.' +
                                  event.im.current_state.name);
        if (event.data.possible_timeout) {
            event.im.metrics.fire_inc('possible_timeout_in.' +
                                      event.im.current_state.name);
            var timeouts = self.inc_user_item(event.im.user,
                                              'possible_timeouts');
            if (timeouts <= 1) {
                self.send_sms_first_possible_timeout(event.im);
            }
        }
    };

    self.on_new_user = function(event) {
        event.im.metrics.fire_inc('unique_users');
    };

    self.on_state_enter = function(event) {
        event.im.metrics.fire_inc('state_entered.' + event.data.state.name);
    };

    self.on_state_exit = function(event) {
        event.im.metrics.fire_inc('state_exited.' + event.data.state.name);
    };

    // Session 1

        self.add_state(new LanguageChoice(
            "intro",
            function (choice) {
                if (self.check_from_addr(this.im)) {
                    return "cadre";
                }
                return "no_vodacom_sim";
            },
            _.gettext("Welcome to the Health Network brought to you by" +
                      " Switchboard, the Ministry of Health and Vodacom." +
                      "\n\n" +
                      "What language would you like to use?"),
            [
                new Choice("sw", "Swahili"),
                new Choice("en", "English")
            ],
            _.gettext("Please select a valid language.")
        ));
        self.add_state(new EndState(
            "no_vodacom_sim",
            _.gettext("Sorry. This service is only available to Health" +
                      " Practitioners with a Vodacom Sim card. Please" +
                      " register a new Vodacom SIM and then redial this" +
                      " number."),
            "intro"
        ));
        self.add_creator("cadre", function(state_name, im) {
            var _ = im.i18n;
            var swb_api = self.swb_api(im);
            var p = swb_api.list_cadres();
            p.add_callback(function (cadres) {
                var choices = cadres.map(function (c) {
                    return new Choice(c.id, c.text);
                });
                choices[choices.length] = new Choice("other",
                                                     _.gettext("Other"));
                return new PaginatedChoiceState(
                    state_name,
                    function (choice) {
                        return (choice.value == "other" ?
                                "cadre_other" :
                                "cheque_number");
                    },
                    _.gettext("What CADRE are you?"),
                    choices, null, {},
                    {
                        more: im.i18n.gettext("View more"),
                        back: im.i18n.gettext("Back"),
                        options_per_page: self.options_per_page,
                        characters_per_page: self.characters_per_page
                    }
                );
            });
            return p;
        });
        self.add_state(new FreeText(
            "cadre_other",
            "cadre_unavailable",
            _.gettext("Please specify your CADRE:")
        ));
        self.add_state(new ChoiceState(
            "cadre_unavailable",
            function (choice, done) {
                if (choice.value == "yes") {
                    var im = this.im;
                    var swb_api = self.swb_api(im);
                    var cadre_name = im.get_user_answer("cadre_other");
                    var p = swb_api.submit_unknown_cadre(im.user_addr,
                                                         cadre_name);
                    p.add_callback(function (result) {
                        done("cadre_unavailable_contact");
                    });
                }
                else {
                    done("cadre_unavailable_dont_contact");
                }
            },
            _.gettext("Sorry but this service is not yet available for" +
                      " your CADRE. Would you like us to contact you if this" +
                      " service becomes available for you in the future?"),
            [
                new Choice("yes", _.gettext("Yes")),
                new Choice("no", _.gettext("No"))
            ]
        ));
        self.add_state(new EndState(
            "cadre_unavailable_contact",
            _.gettext("Thank you for trying to register, we will contact" +
                      " you when the programme is available for your cadre."),
            "intro"
        ));
        self.add_state(new EndState(
            "cadre_unavailable_dont_contact",
            _.gettext("Thank you for trying to register."),
            "intro"
        ));
        self.add_state(new EndState(
            "end", // to support old users who ended sessions in this state
            _.gettext("Thank you for trying to register."),
            "intro"
        ));
        self.add_state(new FreeText(
            "cheque_number",
            function (content) {
                return (content == '0' ?
                        // "registration_number" : "date_of_birth");
                        "registration_number" : "first_name");
            },
            _.gettext("If u R a government Health Worker please enter the" +
                      " last 7-9 digits of Ur cheque no (eg 1234567)." +
                      " If U don't have a cheque no enter '0'"),
            function (content) {
                return Boolean(content.match("^([0-9]{7,9}|[0Oo])$"));
            },
            _.gettext("Sorry but that is not a valid cheque number." +
                      " Please try again of enter '0' if you do not" +
                      " have a cheque number.")
        ));
        self.add_state(new FreeText(
            "registration_number",
            // "date_of_birth",
            "first_name",
            _.gettext("To verify that u r a registered health worker enter" +
                      " your REGISTRATION No (eg 1234) from Medical" +
                      " Council of Tanganyika or enter '0' if u do not have" +
                      " a reg no"),
            function (content) {
                return Boolean(content.match("^([0-9]{1,5}|[0Oo])$"));
            },
            _.gettext("Sorry but that is not a valid Registration Number." +
                      " Please try again or enter 0 if you do not have a" +
                      " registration number.")
        ));
        // Note: Date of birth state disabled at SwB's request
        self.add_state(new FreeText(
            "date_of_birth",
            function (content, done) {
                var swb_api = self.swb_api(this.im);
                var p = swb_api.check_date_of_birth(this.im.user, content);
                p.add_callback(function (result) {
                    if (result) {
                        done("first_name");
                    }
                    else {
                        done("dont_match_mct");
                    }
                });
            },
            _.gettext("Please enter your date of birth in the following" +
                      " format DDMMYYYY (eg if born on 23 April 1980," +
                      " enter 23041980)."),
            function (content) {
                if (!content.match("^[0-9]{8}$"))
                    return false;
                var day = Number(content.slice(0, 2));
                var month = Number(content.slice(2, 4));
                var year = Number(content.slice(4, 8));
                var year_now = (new Date()).getFullYear();
                if (day > 31 || month > 12 || year_now - year < 16)
                    return false;
                return true;
            },
            _.gettext("Sorry but you have not entered a valid date." +
                      " Please enter your date of birth in the following" +
                      " format DDMMYYYY (e.g. if born on 23 April 1980," +
                      " enter 23041980).")
        ));
        self.add_state(new ChoiceState(
            "dont_match_mct",
            function (choice) {
                return (choice.value == "again" ?
                        "cheque_number" :
                        "dont_match_mct_end");
            },
            _.gettext("Sorry but the registration number and DOB that you" +
                      " have entered do not match with MCT records."),
            [
                new Choice("again", _.gettext("Enter details again")),
                new Choice("end", _.gettext("End Session"))
            ]
        ));
        self.add_state(new EndState(
            "dont_match_mct_end",
            _.gettext("Sorry but we cannot verify you at this point. " +
                      "Please verify your details with MCT and dial " +
                      "*149*24# again to register."),
            "intro"
        ));
        self.add_state(new FreeText(
            "first_name",
            "surname",
            _.gettext("Please enter your first name.")
        ));
        self.add_state(new FreeText(
            "surname",
            "terms_and_conditions",
            _.gettext("Please enter your surname.")
        ));
        self.add_state(new ChoiceState(
            "terms_and_conditions",
            function (choice) {
                return (choice.value == "yes" ?
                        "session1_end" :
                        "session1_abort_yn");
            },
            _.gettext("Do you agree to the terms and conditions as laid" +
                      " out at http://www.healthnetwork.or.tz ?" +
                      " Your local DMO will also have a copy."),
            [
                new Choice("yes", _.gettext("Yes")),
                new Choice("no", _.gettext("No"))
            ]
        ));
        self.add_state(new ChoiceState(
            "session1_abort_yn",
            function (choice) {
                return (choice.value == "yes" ?
                        "session1_abort" :
                        "terms_and_conditions");
            },
            _.gettext("We are sorry but you cannot be registered unless" +
                      " you agree to the terms and conditions. Are you" +
                      " sure you would like to end the registration process?"),
            [
                new Choice("yes", _.gettext("Yes")),
                new Choice("no", _.gettext("No"))
            ]
        ));
        self.add_state(new EndState(
            "session1_abort",
            _.gettext("If you would like to register at a later date" +
                      " please dial *149*24#."),
            "intro",
            {
                on_enter: function () {
                    return self.send_sms_session1_abort(this.im);
                }
            }
        ));
        self.add_state(new EndState(
            "session1_end",
            _.gettext("Thank you. You have almost completed your " +
                      "registration process. Please dial *149*24# " +
                      "again to complete just a few more questions."),
            "session2_intro",
            {
                on_enter: function () {
                    var im = this.im;
                    self.incr_metric(im, 'first_session_completed');
                    self.send_sms_session1_end(im);
                }
            }
        ));

    // Session 2

        self.facility_or_district_select = function (content, done) {
            var swb_api = self.swb_api(this.im);
            var im = this.im;
            var p = swb_api.list_districts(content);
            p.add_callback(function (districts) {
                if (districts.length == 1) {
                    im.set_user_answer("district_select",
                                       districts[0].id);
                    done("facility_type");
                }
                else {
                    done("district_select");
                }
            });
        };

        self.add_state(new FreeText(
            "session2_intro",
            self.facility_or_district_select,
            _.gettext("Welcome back 2 the Health Network brought 2 U by " +
                      "Switchboard, the Ministry of Health and Vodacom." +
                      "\n\n" +
                      "Please enter your district (eg Kilosa). No" +
                      " abbreviations please."),
            null, null, {
                on_enter: function () {
                    // clear district_reenter value on state enter
                    // in case a user gets to re-use the menu
                    this.im.set_user_answer("district_reenter", null);
                }
            }
        ));
        self.add_creator("district_select", function(state_name, im) {
            var _ = im.i18n;
            var swb_api = self.swb_api(im);
            var first_query = im.get_user_answer("session2_intro");
            var second_query = im.get_user_answer("district_reenter");
            var query = second_query || first_query;
            var p = swb_api.list_districts(query);
            p.add_callback(function (districts) {
                var choices = districts.map(function (d) {
                    return new Choice(d.id, d.text);
                });
                choices[choices.length] = new Choice(
                    null, _.gettext("None of the above"));

                return new PaginatedChoiceState(
                    state_name,
                    function (choice) {
                        if (choice.value === null && !second_query) {
                            return "district_reenter";
                        }
                        return "facility_type";
                    },
                    _.gettext("The district you entered cannot be found." +
                              " Did you mean:"),
                    choices, null, {},
                    {
                        more: im.i18n.gettext("View more"),
                        back: im.i18n.gettext("Back"),
                        options_per_page: self.options_per_page,
                        characters_per_page: self.characters_per_page
                    }
                );
            });
            return p;
        });
        self.add_state(new FreeText(
            "district_reenter",
            self.facility_or_district_select,
            _.gettext("Please re-enter your district:")
        ));
        self.add_creator("facility_type", function(state_name, im) {
            var _ = im.i18n;
            var swb_api = self.swb_api(im);
            var p = swb_api.list_facility_types();
            p.add_callback(function (facility_types) {
                var choices = facility_types.map(function (f) {
                    return new Choice(f.id, f.text);
                });
                choices[choices.length] = new Choice(null,
                                                     _.gettext("Other"));
                return new PaginatedChoiceState(
                    state_name,
                    "facility_name",
                    _.gettext("Please enter your facility type:"),
                    choices, null, {},
                    {
                        more: im.i18n.gettext("View more"),
                        back: im.i18n.gettext("Back"),
                        options_per_page: self.options_per_page,
                        characters_per_page: self.characters_per_page
                    }
                );
            });
            return p;
        });
        self.add_state(new FreeText(
            "facility_name",
            function (content, done) {
                var im = this.im;
                var swb_api = self.swb_api(im);
                var district = im.get_user_answer("district_select");
                var facility_type = im.get_user_answer("facility_type");
                var query = content;
                var p = swb_api.list_facilities(district, facility_type, query);
                p.add_callback(function (facilities) {
                    var ev_p = success();
                    if (facilities.length == 1) {
                        im.set_user_answer("facility_select", facilities[0].id);
                        ev_p.add_callback(function (result) {
                            var cadre_id = im.get_user_answer("cadre");
                            return swb_api.cadre_needs_specialties(cadre_id);
                        });
                        p.add_callback(function (needs_specialties) {
                            if (needs_specialties) {
                                done("select_speciality");
                            }
                            else {
                                // done("email");
                                done("session2_end");
                            }
                        });
                    }
                    else {
                        ev_p.add_callback(function (result) {
                            done("facility_select");
                        });
                    }
                    return ev_p;
                });
            },
            _.gettext("Please enter the official name of the facility where" +
                      " you primarily practice")
        ));
        self.add_creator("facility_select", function (state_name, im) {
            var _ = im.i18n;
            var swb_api = self.swb_api(im);
            var district = im.get_user_answer("district_select");
            var facility_type = im.get_user_answer("facility_type");
            var query = im.get_user_answer("facility_name");
            var p = swb_api.list_facilities(district, facility_type, query);
            p.add_callback(function (facilities) {
                var choices = facilities.map(function (f) {
                    return new Choice(f.id, f.text);
                });
                choices[choices.length] = new Choice(
                    null, im.i18n.gettext("None of the above"));

                return new PaginatedChoiceState(
                    state_name,
                    function (choice, done) {
                        var p = success();
                        if (choice.value === null) {
                            p.add_callback(function (result) {
                                return swb_api.submit_unknown_facility(
                                    im.user_addr, query, district,
                                    facility_type);
                            });
                        }
                        p.add_callback(function (result) {
                            var cadre_id = im.get_user_answer("cadre");
                            return swb_api.cadre_needs_specialties(cadre_id);
                        });
                        p.add_callback(function (needs_specialties) {
                            if (needs_specialties) {
                                done("select_speciality");
                            }
                            else {
                                // done("email");
                                done("session2_end");
                            }
                        });
                    },
                    _.gettext("The Facility you entered cannot be found." +
                              " Did you mean:"),
                    choices, null, {},
                    {
                        more: im.i18n.gettext("View more"),
                        back: im.i18n.gettext("Back"),
                        options_per_page: self.options_per_page,
                        characters_per_page: self.characters_per_page
                    }
                );
            });
            return p;
        });
        self.add_creator("select_speciality", function (state_name, im) {
            var _ = im.i18n;
            var swb_api = self.swb_api(im);
            var cadre_id = im.get_user_answer("cadre");
            var p = swb_api.list_specialities(cadre_id);
            p.add_callback(function (specialities) {
                var choices = specialities.map(function (s) {
                    return new Choice(s.id, s.text);
                });
                choices[choices.length] = new Choice(null,
                                                     _.gettext("Other"));
                return new PaginatedChoiceState(
                    state_name,
                    // "email",
                    "session2_end",
                    _.gettext("Please enter your specialty:"),
                    choices, null, {},
                    {
                        more: im.i18n.gettext("View more"),
                        back: im.i18n.gettext("Back"),
                        options_per_page: self.options_per_page,
                        characters_per_page: self.characters_per_page
                    }
                );
            });
            return p;
        });
        // Note: Email state disabled at SwB's request
        self.add_state(new FreeText(
            "email",
            "session2_end",
            _.gettext("Please enter your email address or 0 if you do not " +
                      "have an email address"),
            function (content) {
                return Boolean(content.match("^(.+\\@.+\\..+|[0Oo])$"));
            },
            _.gettext("Sorry but that is not a valid email address. Please" +
                      " try again or enter 0 if you do not have an email" +
                      " address.")
        ));
        // Note: confirmation SMSes need to be sent manually via Vumi Go
        //       once the registration has been approved and activated.

        self.create_health_worker = function (im) {
            var ans = im.get_user_answer;
            var swb_api = self.swb_api(im);
            var hw = {};

            hw.full_name = ans("first_name") + " " + ans("surname");
            hw.surname = ans("surname");
            hw.specialties = [ans("cadre")];
            var specialty = ans("select_speciality");
            if (specialty)
                hw.specialties[hw.specialties.length] = specialty;
            var cheque_number = ans("cheque_number");
            if (cheque_number && !cheque_number.match('^[0Oo]$'))
                hw.cheque_number = cheque_number;
            var registration_number = ans("registration_number");
            if (registration_number && !registration_number.match('^[0Oo]$'))
                hw.registration_number = registration_number;
            var date_of_birth = ans("date_of_birth");
            if (date_of_birth)
                hw.birthdate = swb_api.dob_to_obj(date_of_birth);
            hw.country = "TZ";
            var email = ans("email");
            hw.email = (email && !email.match('^[0Oo]$')) ? email : null;
            hw.facility = ans("facility_select");
            hw.vodacom_phone = im.user_addr;

            return hw;
        };

        self.add_state(new EndState(
            "session2_end",
            _.gettext("Thank you for registering with The Health Network" +
                      " Programme. We will process your registration and" +
                      " confirm with you when you are registered."),
            function (content) {
                if (self.qa(this.im)) {
                    return "intro";
                }
                return "session2_end";
            },
            {
                on_enter: function () {
                    var im = this.im;
                    var swb_api = self.swb_api(im);
                    var health_worker = self.create_health_worker(im);
                    swb_api.register_health_worker(health_worker);
                    self.set_user_item(im.user, "registered", 1);
                    self.send_sms_session2_end(im);
                    self.incr_metric(im, 'second_session_completed');
                    var sessions = self.get_user_item(im.user,
                                                      'ussd_sessions', 0);
                    im.metrics.fire_avg('sessions_taken_to_register',
                                        sessions);
                }
            }
        ));

}


// launch app
var states = new RegisterHealthWorker();
var im = new InteractionMachine(api, states);
{'fraction_non_alphanumeric': 0.11087396340514462, 'fraction_numerical': 0.0060694990995798035, 'mean_word_length': 1.3732752934969001, 'pattern_counts': {'":': 3, '<': 21, '<?xml version=': 0, '>': 21, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 68, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1515461', 'n_tokens_mistral': 25324, 'n_tokens_neox': 23841, 'n_words': 5531}