Exemple #1
0
 def process_send_token_request(self, user_input):
     user = get_user_by_phone(self.session.get_data('recipient_phone'),
                              "KE")
     amount = float(self.session.get_data('transaction_amount'))
     reason_str = self.session.get_data('transaction_reason_i18n')
     reason_id = float(self.session.get_data('transaction_reason_id'))
     ussd_tasker.send_token(self.user, user, amount, reason_str, reason_id)
Exemple #2
0
    def post(self):
        post_data = request.get_json() or request.form

        session_id = post_data.get('sessionId')
        phone_number = post_data.get('phoneNumber')
        user_input = post_data.get('text')
        service_code = post_data.get('serviceCode')

        if phone_number:
            user = get_user_by_phone(phone_number)
            # api chains all inputs that came through with *
            latest_input = user_input.split('*')[-1]
            if None in [user, session_id]:
                current_menu = UssdMenu.find_by_name('exit_not_registered')
                text = menu_display_text_in_lang(current_menu, user)
            else:
                current_menu = UssdProcessor.process_request(session_id, latest_input, user)
                ussd_session = create_or_update_session(session_id, user, current_menu, user_input, service_code)
                text = UssdProcessor.custom_display_text(current_menu, ussd_session)

                if "CON" not in text and "END" not in text:
                    raise Exception("no menu found. text={}, user={}, menu={}, session={}".format(text, user.id, current_menu.name, ussd_session.id))

                if len(text) > 164:
                    print(f"Warning, text has length {len(text)}, display may be truncated")

                db.session.commit()
        else:
            current_menu = UssdMenu.find_by_name('exit_invalid_request')
            text = menu_display_text_in_lang(current_menu, None)

        return make_response(text), 200
Exemple #3
0
 def process_exchange_token_request(self, user_input):
     agent = get_user_by_phone(self.session.get_data('agent_phone'), "KE")
     amount = float(self.session.get_data('exchange_amount'))
     ussd_tasker.exchange_token(self.user, agent, amount)
Exemple #4
0
 def is_valid_token_agent(self, user_input):
     user = get_user_by_phone(user_input)
     return user is not None and user.has_token_agent_role
Exemple #5
0
 def is_token_agent(self, user_input):
     user = get_user_by_phone(user_input)
     return self.is_valid_recipient(user, True, True)
Exemple #6
0
 def is_user(self, user_input):
     user = get_user_by_phone(user_input)
     return self.is_valid_recipient(user, True, False)
Exemple #7
0
    def custom_display_text(menu: UssdMenu, ussd_session: UssdSession) -> str:
        """
        Many USSD responses include user-specific data that is stored inside the USSD session. This function
        extracts the appropriate session data based on the current menu name and then inserts them as keywords in the
        i18n function.
        :param menu: The USSD menu to create a text response for
        :param ussd_session: The ussd session containing user data
        :return: raw ussd menu text string
        """

        user = ussd_session.user

        if menu.name == 'about_my_business':
            bio = next(
                filter(lambda x: x.name == 'bio', user.custom_attributes),
                None)
            if bio:
                bio_text = bio.value.strip('"')
            else:
                bio_text = None

            if bio_text is None or '':
                return i18n_for(user, "{}.none".format(menu.display_key))
            else:
                return i18n_for(user,
                                "{}.bio".format(menu.display_key),
                                user_bio=bio_text)

        if menu.name == 'send_token_confirmation':
            recipient = get_user_by_phone(
                ussd_session.get_data('recipient_phone'), should_raise=True)
            recipient_phone = recipient.user_details()
            token = default_token(user)
            transaction_amount = ussd_session.get_data('transaction_amount')
            transaction_reason = ussd_session.get_data(
                'transaction_reason_i18n')
            return i18n_for(
                user,
                menu.display_key,
                recipient_phone=recipient_phone,
                token_name=token.symbol,
                transaction_amount=cents_to_dollars(transaction_amount),
                transaction_reason=transaction_reason)

        if menu.name == 'exchange_token_confirmation':
            agent = get_user_by_phone(ussd_session.get_data('agent_phone'),
                                      should_raise=True)
            agent_phone = agent.user_details()
            token = default_token(user)
            exchange_amount = ussd_session.get_data('exchange_amount')
            return i18n_for(user,
                            menu.display_key,
                            agent_phone=agent_phone,
                            token_name=token.symbol,
                            exchange_amount=cents_to_dollars(exchange_amount))

        # in matching is scary since it might pick up unintentional ones
        if 'exit' in menu.name or 'help' == menu.name:
            return i18n_for(user,
                            menu.display_key,
                            support_phone='+254757628885')

        # in matching is scary since it might pick up unintentional ones
        if 'pin_authorization' in menu.name or 'current_pin' == menu.name:
            if user.failed_pin_attempts is not None and user.failed_pin_attempts > 0:
                return i18n_for(user,
                                "{}.retry".format(menu.display_key),
                                remaining_attempts=3 -
                                user.failed_pin_attempts)
            else:
                return i18n_for(user, "{}.first".format(menu.display_key))

        if menu.name == 'directory_listing' or menu.name == 'send_token_reason':

            blank_template = i18n_for(user, menu.display_key, options='')

            blank_len = len(blank_template)

            most_relevant_usages = ussd_session.get_data(
                'transfer_usage_mapping')

            options = UssdProcessor.fit_usages(ussd_session,
                                               most_relevant_usages, blank_len,
                                               user, 0, [0])

            # current_usages = most_relevant_usages[:ITEMS_PER_MENU]
            return i18n_for(user, menu.display_key, options=options)

        if menu.name == 'directory_listing_other' or menu.name == 'send_token_reason_other':

            most_relevant_usages = ussd_session.get_data(
                'transfer_usage_mapping')
            usage_menu_nr = ussd_session.get_data('usage_menu')
            usage_stack = ussd_session.get_data('usage_index_stack') or [0]

            start_of_list = usage_stack[usage_menu_nr]

            total_usages = len(most_relevant_usages)

            # First see if we can fit remaining usages onto the one page
            if start_of_list + ITEMS_PER_MENU > total_usages:
                part = 'first' if start_of_list == 0 else 'last'
                current_usages = most_relevant_usages[
                    start_of_list:total_usages]
                menu_options = UssdProcessor.create_usages_list(
                    current_usages, user)

                translated_menu = i18n_for(user,
                                           "{}.{}".format(
                                               menu.display_key, part),
                                           other_options=menu_options)

                if len(translated_menu) <= USSD_MAX_LENGTH:
                    return translated_menu

            # Oh well, guess we just have to fit as many as possible then

            part = 'first' if start_of_list == 0 else 'middle'

            blank_template = i18n_for(user,
                                      "{}.{}".format(menu.display_key, part),
                                      other_options='')

            blank_len = len(blank_template)

            options = UssdProcessor.fit_usages(ussd_session,
                                               most_relevant_usages, blank_len,
                                               user, start_of_list,
                                               usage_stack)

            # current_usages = most_relevant_usages[:ITEMS_PER_MENU]
            return i18n_for(user,
                            "{}.{}".format(menu.display_key, part),
                            other_options=options)

        return i18n_for(user, menu.display_key)
    def custom_display_text(menu: UssdMenu, ussd_session: UssdSession) -> str:
        """
        Many USSD responses include user-specific data that is stored inside the USSD session. This function
        extracts the appropriate session data based on the current menu name and then inserts them as keywords in the
        i18n function.
        :param menu: The USSD menu to create a text response for
        :param ussd_session: The ussd session containing user data
        :return: raw ussd menu text string
        """

        user = ussd_session.user

        if menu.name == 'about_me':
            bio = next(filter(lambda x: x.name == 'bio', user.custom_attributes), None)
            first_name = user.first_name
            last_name = user.last_name
            gender = next(filter(lambda x: x.name == 'gender', user.custom_attributes), None)
            location = user.location

            if bio and gender:
                bio_text = bio.value.strip('"')
                gender_text = gender.value.strip('"')
            else:
                bio_text = None
                gender_text = None

            # translations
            absent_value_placeholder = "missing"
            if user.preferred_language == "sw":
                absent_value_placeholder = 'hakuna'
                if gender_text == 'male':
                    gender_text = 'mwanaume'
                elif gender_text == 'female':
                    gender_text = 'mwanamke'

            if first_name == 'Unknown first name':
                first_name = None

            if last_name == 'Unknown last name':
                last_name = None

            if bio_text == 'Unknown business':
                bio_text = None

            if gender_text == 'Unknown gender':
                gender_text = None

            if location == 'Unknown location':
                location = None

            # define final values to show in menu
            first_name = first_name or absent_value_placeholder
            last_name = last_name or absent_value_placeholder
            bio_text = bio_text or absent_value_placeholder
            gender_text = gender_text or absent_value_placeholder
            location = location or absent_value_placeholder

            full_name = "{} {}".format(first_name, last_name)
            if first_name == absent_value_placeholder and last_name == absent_value_placeholder:
                full_name = absent_value_placeholder

            return i18n_for(user, "{}.profile".format(menu.display_key),
                            full_name=full_name,
                            gender=gender_text,
                            location=location, user_bio=bio_text)

        if menu.name == 'send_token_pin_authorization':
            recipient = get_user_by_phone(ussd_session.get_data('recipient_phone'), 'KE', True)
            other_user_details = recipient.user_details()
            user_details = user.user_details()
            token = default_token(user)
            transaction_amount = ussd_session.get_data('transaction_amount')
            if user.failed_pin_attempts > 0:
                return i18n_for(
                    user=user,
                    key="{}.{}".format(menu.display_key, 'retry'),
                    remaining_attempts=3 - user.failed_pin_attempts
                )
            else:
                return i18n_for(
                    user=user,
                    key="{}.{}".format(menu.display_key, 'first'),
                    transaction_amount=cents_to_dollars(transaction_amount),
                    token_name=token.symbol,
                    other_user_details=other_user_details,
                    user_details=user_details

                )

        if menu.name == 'exit_successful_send_token':
            recipient = get_user_by_phone(ussd_session.get_data('recipient_phone'), 'KE', True)
            other_user_details = recipient.user_details()
            user_details = user.user_details()
            token = default_token(user)
            transaction_amount = ussd_session.get_data('transaction_amount')
            return i18n_for(
                user=user,
                key="{}".format(menu.display_key, 'exit_successful_send_token'),
                transaction_amount=cents_to_dollars(transaction_amount),
                token_name=token.symbol,
                other_user_details=other_user_details,
                user_details=user_details

            )

        if menu.name == 'exchange_token_confirmation':
            agent = get_user_by_phone(ussd_session.get_data('agent_phone'), 'KE', True)
            agent_phone = agent.user_details()
            token = default_token(user)
            exchange_amount = ussd_session.get_data('exchange_amount')
            return i18n_for(
                user, menu.display_key,
                agent_phone=agent_phone,
                token_name=token.symbol,
                exchange_amount=cents_to_dollars(exchange_amount)
            )

        # in matching is scary since it might pick up unintentional ones
        if 'exit' in menu.name or 'help' == menu.name:
            return i18n_for(
                user, menu.display_key,
                support_phone='+254757628885'
            )

        # in matching is scary since it might pick up unintentional ones
        if 'pin_authorization' in menu.name or 'current_pin' == menu.name:
            if user.failed_pin_attempts is not None and user.failed_pin_attempts > 0:
                return i18n_for(
                    user, "{}.retry".format(menu.display_key),
                    remaining_attempts=3 - user.failed_pin_attempts
                )
            else:
                return i18n_for(user, "{}.first".format(menu.display_key))

        if menu.name == 'directory_listing' or menu.name == 'send_token_reason':

            blank_template = i18n_for(
                user, menu.display_key, options=''
            )

            blank_len = len(blank_template)

            most_relevant_usages = ussd_session.get_data('transfer_usage_mapping')

            options = KenyaUssdProcessor.fit_usages(
                ussd_session,
                most_relevant_usages,
                blank_len,
                user,
                0,
                [0]
            )

            # current_usages = most_relevant_usages[:ITEMS_PER_MENU]
            return i18n_for(
                user, menu.display_key,
                options=options
            )

        if menu.name == 'directory_listing_other' or menu.name == 'send_token_reason_other':

            most_relevant_usages = ussd_session.get_data('transfer_usage_mapping')
            usage_menu_nr = ussd_session.get_data('usage_menu')
            usage_stack = ussd_session.get_data('usage_index_stack') or [0]

            start_of_list = usage_stack[usage_menu_nr]

            total_usages = len(most_relevant_usages)

            # First see if we can fit remaining usages onto the one page
            if start_of_list + ITEMS_PER_MENU > total_usages:
                part = 'first' if start_of_list == 0 else 'last'
                current_usages = most_relevant_usages[start_of_list:total_usages]
                menu_options = KenyaUssdProcessor.create_usages_list(current_usages, user)

                translated_menu = i18n_for(
                    user, "{}.{}".format(menu.display_key, part),
                    other_options=menu_options
                )

                if len(translated_menu) <= USSD_MAX_LENGTH:
                    return translated_menu

            # Oh well, guess we just have to fit as many as possible then

            part = 'first' if start_of_list == 0 else 'middle'

            blank_template = i18n_for(
                user, "{}.{}".format(menu.display_key, part),
                other_options=''
            )

            blank_len = len(blank_template)

            options = KenyaUssdProcessor.fit_usages(
                ussd_session,
                most_relevant_usages,
                blank_len,
                user,
                start_of_list,
                usage_stack)

            # current_usages = most_relevant_usages[:ITEMS_PER_MENU]
            return i18n_for(
                user, "{}.{}".format(menu.display_key, part),
                other_options=options
            )

        return i18n_for(user, menu.display_key)
    def post(self):
        post_data = request.get_json() or request.form

        session_id = post_data.get('sessionId')
        phone_number = post_data.get('phoneNumber')
        user_input = post_data.get('text')
        service_code = post_data.get('serviceCode')

        # enforce only one single service code that can access the ussd state machine
        # through the endpoint
        if config.USSD_VALID_SERVICE_CODE != service_code:
            response = 'END '
            response += i18n.t(
                'ussd.kenya.invalid_service_code',
                valid_service_code=config.USSD_VALID_SERVICE_CODE,
                locale='sw')
            response += "\n"
            response += i18n.t(
                'ussd.kenya.invalid_service_code',
                valid_service_code=config.USSD_VALID_SERVICE_CODE,
                locale='en')
            return make_response(response, 200)

        elif phone_number:
            user = get_user_by_phone(phone_number, 'KE')
            # api chains all inputs that came through with *
            latest_input = user_input.split('*')[-1]
            if None in [user, session_id]:
                user_without_transfer_account = create_user_without_transfer_account(
                    phone_number)
                current_menu = UssdMenu.find_by_name(
                    'initial_language_selection')
                ussd_session = create_or_update_session(
                    session_id=session_id,
                    user=user_without_transfer_account,
                    user_input=latest_input,
                    service_code=service_code,
                    current_menu=current_menu)
                text = KenyaUssdProcessor.custom_display_text(
                    current_menu, ussd_session)
            else:
                current_menu = KenyaUssdProcessor.process_request(
                    session_id, latest_input, user)
                ussd_session = create_or_update_session(
                    session_id, user, current_menu, user_input, service_code)
                text = KenyaUssdProcessor.custom_display_text(
                    current_menu, ussd_session)

                if "CON" not in text and "END" not in text:
                    raise Exception(
                        "no menu found. text={}, user={}, menu={}, session={}".
                        format(text, user.id, current_menu.name,
                               ussd_session.id))

                if len(text) > 164:
                    print(
                        f"Warning, text has length {len(text)}, display may be truncated"
                    )

                db.session.commit()

        else:
            current_menu = UssdMenu.find_by_name('exit_invalid_request')
            text = menu_display_text_in_lang(current_menu, None)

        return make_response(text), 200