Creating ussd screens

This document is a whirlwind tour of how to create ussd screen.

Strong feature of ussd airflow is to create ussd screen via yaml and not code. This make it easier to give the product owners to design ussd without knowing how to code

In ussd airflow customer journey is created via yaml. Each section in a yaml defines a ussd screen. There different types of ussd and each type has its own rule on how to write ussd application

Common rule in creating any kind of screen Each screen has field called “type” apart from initial_screen

The following are types of ussd and the rules to write them.

1. Initial screen (type -> initial_screen)

class ussd.screens.initial_screen.InitialScreen(ussd_request: ussd.core.UssdRequest, handler: str, screen_content: dict, initial_screen: dict, logger=None)[source]

This screen is mandatory in any customer journey. It is the screen all new ussd session go to.

example of one

initial_screen: enter_height

first_screen:
type: quit
text: This is the first screen

Its is also used to define variable file if you have one. Example when defining variable file

initial_screen:
    screen: screen_one
    variables:
        file: /path/of/your/variable/file.yml
        namespace: used_to_save_the_variable

Sometimes you want to send ussd session to some 3rd party application when the session has been terminated.

We can easily do that at end of session i.e quit screen, But for those scenarios where session is terminated by user or mno we don’t know that unless the mno send us a request.

Most mnos don’t send notifier 3rd party application about the session being dropped. The work around we use is schedule celery task to run after 15 minutes ( by that time we know there is no active session)

Below is an example of how to schedule a ussd report session after 15min

example:

initial_screen:
    type: initial_screen
    next_screen: screen_one
    ussd_report_session:
        session_key: reported
        retry_mechanism:
            max_retries: 3
        validate_response:
            - expression: "{{reported.status_code}} == 200"
        request_conf:
            url: localhost:8006/api
            method: post
            data:
                ussd_interaction: "{{ussd_interaction}}"
                session_id: "{{session_id}}"
        async_parameters:
            queue: report_session
            countdown: 900
Lets explain the variables in ussd_report_session
  • session_key ( Mandatory )
    response of ussd report session would be saved under that key in session store
  • request_conf ( Mandatory )
    Those are the parameters to be used to make request to report ussd session
  • validate_response ( Mandatory )
    After making ussd report request the framework will evaluate your options and if one of them is valid it would mark session as posted (This is used to avoid double ussd submission)
  • retry_mechanism ( Optional )
    After validating your response and all of them fail we will go ahead and retry if this field is active.
  • async_parameters ( Optional )
    This is are the parameters used to make ussd request

2. Input screen (type -> input_screen)

class ussd.screens.input_screen.InputScreen(*args, **kwargs)[source]

This screen prompts the user to enter an input

Fields required:
  • text: this the text to display to the user.

  • input_identifier: input amount entered by users will be saved

    with this key. To access this in the input anywhere {{ input_identifier }}

  • next_screen: The next screen to go after the user enters

    input

  • validators:
    • text: This is the message to display when the validation fails regex: regex used to validate ussd input. Its mutually exclusive with expression
    • expression: if regex is not enough you can use a jinja expression
    will be called ussd request object

    text: This the message thats going to be displayed if expression returns False

  • options (This field is optional):

    This is a list of options to display to the user each option is a key value pair of option text to display and next_screen to redirect if option is selected. Example of option:

    options:
     - text: option one
       next_screen: screen_one
     - text: option two
       next_screen: screen_two
    
Example:
initial_screen:
  type: initial_screen
  next_screen: enter_height
  default_language: en

enter_height:
  type: input_screen
  text:
    en: |
      Enter your height
    sw: |
      Weka ukubwa lako
  input_identifier: height
  default_next_screen: enter_age
  next_screen:
    - condition: input|int == 60
      next_screen: height_above_60
    - condition: input|int == 30
      next_screen: height_below_30
  validators:
    - regex: ^[0-9]{1,7}$
      text:
        en: |
          Enter number between 1 and 7
        sw: |
          Weka namba kutoka 1 hadi 7

enter_age:
    type: input_screen
    text:
      en: |
        Enter your age
      sw: |
        Weka miaka yako
    input_identifier: age
    next_screen: show_information
    options:
      - text:
          en: back
          sw: rudi
        next_screen: enter_height
    validators:
        - regex: ^[0-9]{1,7}$
          text:
            en: |
              Only nubers are allowed
            sw: |
              Nambari pekee ndio zimekubalishwa
            default: en
        - expression: ussd_request.input|int < 100
          text:
            en: |
              Number over 100 is not allowed
            sw: |
              Nambari juu ya 100 haikubalishwi

show_information:
    text:
      en: |
        Your age is {{ age }} and your height is {{ height }}.
        Enter anything to go back to the first screen
      sw: |
        Miaka yako in {{ age }} na ukubwa wako in {{ height }}.
        Weka kitu ingine yoyote unende kwenye screen ya kwanza
    type: input_screen
    input_identifier: foo
    next_screen: enter_height

height_above_60:
  type: quit_screen
  text: We are not interested with height above 60

height_below_30:
  type: quit_screen
  text: We are not interested with height below 30

4. Quit screen (type -> quit_screen)

class ussd.screens.quit_screen.QuitScreen(ussd_request: ussd.core.UssdRequest, handler: str, screen_content: dict, initial_screen: dict, logger=None)[source]

This is the last screen to be shown in a ussd session.

Its the easiest screen to create. It requires only text

Example of quit screen:

initial_screen: example_of_quit_screen

example_of_quit_screen:
  type: quit_screen
  text: "Test getting variable from os environmen. {{TEST_VARIABLE}}"

5. Http screen (type -> http_screen)

class ussd.screens.http_screen.HttpScreen(ussd_request: ussd.core.UssdRequest, handler: str, screen_content: dict, initial_screen: dict, logger=None)[source]

This screen is invisible to the user. Its used if you want to make an api call. Its very if you want to make a api call so that you can show the user the results in the next screen.

For instance you can make call for balance check using this screen. And display the balance in the next screen.

Fields used to create this screen:

  1. http_request

    This field contains all the fields used to make http request. It contains the following fields:

    1. method
      This is the request method to use.
      either: get, post, put, delete
    2. url
      This is the url to be used to make the api call
    3. And all the parameters python request module would accept

    you will example below

  2. session_key

    In this screen the api call is expected to return json body. The json body is saved in session using this session_key

  3. synchronous (optional defaults to true) This defines the nature of the api call. If its asynchronous the request will be made later in celery task.

  4. next_screen

    After the api call has been made or been scheduled to celery task ussd request is forwarded to this next_screen

Examples of router screens:

initial_screen: http_get_example

http_get_example:
  type: http_screen
  next_screen: http_get_url_query
  session_key: get_response
  http_request:
    method: get
    url: http://localhost:8000/mock/balance
    params:
      phone_number: "{{ phone_number }}"
      session_id: "{{ session_id }}"
    verify: false
    headers:
      content-type: "application/json"
      user-agent: 'my-app/0.0.1'

http_get_url_query:
  type: http_screen
  next_screen: http_post_example
  session_key: get_url_query
  http_request:
    method: get
    url: "http://localhost:8000/mock/balance/{{phone_number}}/"

http_post_example:
  type: http_screen
  next_screen: http_async_example
  session_key: http_post_response
  http_request:
    method: post
    url: http://localhost:8000/mock/balance
    params:
      phone_numbers:
        - 200
        - 201
        - 202
      session_id: "{{ session_id }}"
    verify: true
    timeout: 30
    headers:
      content-type: "application/json"


http_async_example:
  type: http_screen
  synchronous: True
  next_screen: end_of_http_example
  session_key: http_async_response
  http_request:
    method: get
    url: https://localhost:8000/mock/submission
    params:
      phone_number: "{{ phone_number }}"
      session_id: "{{ session_id }}"

end_of_http_example:
  type: quit_screen
  text: >
    Testing response is being saved in session status code is
    {{http_post_response.status_code}} and balance is
    {{http_post_response.balance}} and full content {{http_post_response.content}}.

6. Router screen (type -> router_screen)

class ussd.screens.router_screen.RouterScreen(ussd_request: ussd.core.UssdRequest, handler: str, screen_content: dict, initial_screen: dict, logger=None)[source]

This screen is invisible to the user. Sometimes you would like to direct user to different screens depending on some status.

For instance you want to show different screen to users who are not registered and a different screen to users who have already registered. This is the screen to create.

Fields used to create this screen:
  1. router_options

    This is a list of router option. Each router option has the following fields

    1. expression
      This is a jinja expression that’s is evaluating to boolean It can reference anything in the session and parameters in ussd_request
    2. next_screen
      This is the screen to direct to if the above expression is true
  2. default_next_screen (optional)

    This is the screen to direct to if all expression in router_options failed.

  3. with_items (optional)

    Sometimes you want to loop over something until an item passes the expression. In this case use with_items. When using with_items you can use variable item in the expression.

    see in the example below for more explanation

Examples of router screens

initial_screen: router_exa_1

router_exa_1:
  type: router_screen
  default_next_screen: default_screen
  router_options:
    - expression: "{{ phone_number == 200|string }}"
      next_screen: 200_phone_number
    - expression: "{{ phone_number == 202| string }}"
      next_screen: 202_phone_number
    - expression: "{{ phone_number in [203|string, 204|string, 205|string] }}"
      next_screen: sample_router_screen_with_loop
    - expression: "{{ phone_number in [ 206|string, 207|string] }}"
      next_screen: sample_router_screen_with_dict_loop


200_phone_number:
  type: quit_screen
  text: This number is 200

202_phone_number:
  type: quit_screen
  text: This number is 202

default_screen:
  type: quit_screen
  text: This is the default screen

sample_router_screen_with_loop:
  type: router_screen
  default_next_screen: default_screen
  with_items: "{{ phone_numbers[phone_number]}}"
  router_options:
    - expression: "{{ item == 'registered' }}"
      next_screen: registred_screen
    - expression: "{{ item == 'not_registered'}}"
      next_screen: not_registered

registred_screen:
  type: quit_screen
  text: You are registered user

not_registered:
  type: quit_screen
  text: You are not registered user

sample_router_screen_with_dict_loop:
  type: router_screen
  default_next_screen: default_screen
  with_items:
    phone_number: '207'
  router_options:
    - expression: '{{ key == "phone_number" and value == phone_number}}'
      next_screen: 207_screen

207_screen:
  type: quit_screen
  text: >
    This screen has been routed here because the
    phone number is {{phone_number}}

7. Update session screen (type -> update_session_screen)

class ussd.screens.update_session_screen.UpdateSessionScreen(ussd_request: ussd.core.UssdRequest, handler: str, screen_content: dict, initial_screen: dict, logger=None)[source]

This screen is invisible to the user. Sometimes you may want to save something to the session to use later in other screens.

Fields used to create this screen:
  1. next_screen

    The screen to go after the session has been saved

  2. values_to_update

    This section defines the session to be saved.

    Inside this section should define the following fields

    1. key
      the key to be used to save
    2. value
      the value to store with the key above
    3. expression
      sometimes you want a condition before you can save data in section
Example:
initial_screen: screen_one

screen_one:
  type: update_session_screen
  next_screen: screen_two
  values_to_update:
    - expression: "{{phone_number == 200|string}}"
      key: customer_status
      value: registered
    - expression: "{{phone_number == 404|string}}"
      key: customer_status
      value: not_registered
    - key: aged_24
      value: "{{[]}}"
    - key: height_54
      value: "{{[]}}"

screen_two:
  type: update_session_screen
  next_screen: show_saved_status
  with_items:
    - name: Francis Mwangi
      age: 24
      height: 5.4
    - name: Isaac Karanja
      age: 22
      height: 5.4
    - name: Stephen Gitigi
      age: 20
      height: 5.5
    - name: Wambui
      age: 24
      height: 5.4
  values_to_update:
    - expression: "{{item.age == 24}}"
      key: aged_24
      value: "{{aged_24|append(item)}}"
    - expression: "{{item.height == 5.4}}"
      key: "height_54"
      value: "{{height_54|append(item)}}"

show_saved_status:
  type: quit_screen
  text: |
    The customer status is {{customer_status}}.
    People aged 24 {{aged_24}}
    People with height 5.4 {{height_54}}

8. Custom screen (type -> custom_screen)

class ussd.screens.custom_screen.CustomScreen(ussd_request: ussd.core.UssdRequest, handler: str, screen_content: dict, initial_screen: dict, logger=None)[source]

If you have a particular user case that’s not yet covered by our existing screens, this is the screen to use.

This screen allows us to define our own ussd screen.

To create it you need the following fields.
  1. screen_object
    This is the path to be used to import the class
  2. serializer (optional)
    This if you want to be validating your screen with specific fields
  3. You can define any field that you feel
    your custom screen might need.
EXAMPLE:
examples of custom screen
class SampleCustomHandler1(UssdHandlerAbstract):
    abstract = True  # don't register custom classes
    @staticmethod
    def show_ussd_content():  # This method doesn't have to be static
        # Do anything custom here.
        return UssdResponse("This is a custom Handler1")

    def handle_ussd_input(self, ussd_input):
        # Do anything custom here
        print(ussd_input)  # pep 8 for the sake of using it.
        return self.ussd_request.forward('custom_screen_2')


class SampleSerializer(UssdBaseSerializer, NextUssdScreenSerializer):
        input_identifier = serializers.CharField(max_length=100)


class SampleCustomHandlerWithSerializer(UssdHandlerAbstract):
    abstract = True  # don't register custom classes
    serializer = SampleSerializer

    @staticmethod
    def show_ussd_content():  # This method doesn't have to be static
        return "Enter a digit and it will be doubled on your behalf"

    def handle_ussd_input(self, ussd_input):
        self.ussd_request.session[
            self.screen_content['input_identifier']
        ] = int(ussd_input) * 2

        return self.ussd_request.forward(
            self.screen_content['next_screen']
        )

example of defining a yaml

initial_screen:
  type: initial_screen
  next_screen: choose_meal
  pagination_config:
    ussd_text_limit: 90
    more_option:
      en: More
    back_option:
      en: Back


choose_meal:
  type: menu_screen
  text: Choose your favourite meal
  error_message: |
    You have selected invalid option try again
  options:
      - text: food
        next_screen: types_of_food
      - text: fruits
        next_screen: types_of_fruit
      - text: drinks
        next_screen: types_of_drinks
      - text: vegetables
        next_screen: types_of_vegetables
      - text: test pagination
        next_screen: test_text_prompt_pagination


types_of_food:
  type: menu_screen
  text: Choose your favourite food
  options:
    - text: rice
      next_screen: rice_chosen
    - text: back
      next_screen: choose_meal
    - text: test next screen routing
      next_screen:
        - condition: phone_number == '200'
          next_screen: test_next_screen_routing_one
        - condition: phone_number == '201'
          next_screen: test_next_screen_routing_two

types_of_fruit:
  type: menu_screen
  text: No fruits available choose * to go back
  options:
    - text: back
      next_screen: choose_meal
      input_value: '*'

types_of_drinks:
  type: menu_screen
  text: No drinks available choose 0 to go back
  options:
    - text: back
      next_screen: choose_meal
      input_display: "0 "
      input_value: '0'

rice_chosen:
  type: menu_screen
  text: Your rice will be delivered shortly. Choose 1 to go back
  options:
    - text: back
      next_screen: choose_meal

types_of_vegetables:
  type: menu_screen
  text: Choose one of the following vegetables
  items:
    text: Vege {{ item }}
    value: "{{ item }}"
    with_items: "{{vegetables_list}}"
    session_key: selected_vegetable
    next_screen: choose_quantity

choose_quantity:
  type: menu_screen
  text: Choose vegetable size
  items:
    text: "{{ key }} at Ksh {{ value }}"
    value:  "{{ key }}"
    with_dict: "{{ vegetable_quantity }}"
    session_key: selected_quantity
    next_screen: selected_vegetable
  options:
    - text: back
      next_screen: choose_meal

selected_vegetable:
  type: menu_screen
  text: >
   You have selected this {{selected_vegetable}}
   and this quantity {{selected_quantity}} at
   {{vegetable_quantity[selected_quantity]}}
  options:
    - text: test_list
      next_screen: test_list_with_native_loop

test_list_with_native_loop:
  type: menu_screen
  text: ""
  items:
    text: "{{item}}"
    value: "{{item}}"
    next_screen: test_explicit_dict_loop
    session_key: alphabet
    with_items:
      - a
      - b
      - c
      - d

test_explicit_dict_loop:
  type: menu_screen
  text: ""
  items:
    text: "{{key}} for {{value}}"
    value: "{{item}}"
    next_screen: test_invalid_jija_variable
    session_key: testing
    with_dict:
      a: apple
      b: boy
      c: cat

# we only support {{ }} jinja variables the otheres will be ingnored
# for now
test_invalid_jija_variable:
  type: menu_screen
  text: Choose one of the following vegetables
  items:
    text: Vege {{ item }}
    value: "{{ item }}"
    with_items: "{%vegetables_list%}"
    session_key: selected_vegetable
    next_screen: choose_quantity


# The screens below are testing pagination
test_text_prompt_pagination:
  type: menu_screen
  text: |
    Ussd airflow should be able to wrap anytext that is larger than the one
    specified into two screens.
  options:
    - text: next
      next_screen: test_pagination_in_menu_options

test_pagination_in_menu_options:
  type: menu_screen
  text: |
    An example of screen with multiple options that need to be paginated
  options:
    - text: screen_with_both_text_and_menu_options_pagination
      next_screen: test_pagination_in_both_text_and_options
    - text: screen_with_both_text_item_options_pagination
      next_screen: test_pagination_in_both_text_options_items

test_pagination_in_both_text_and_options:
  type: menu_screen
  text: |
    This screen has both large text and options that exceed the limit required
    so both the prompt and options will be paginated.
  options:
    - text: go back to the previous screen
      next_screen: test_pagination_in_menu_options
    - text: quit this session
      next_screen: last_screen
    - text: this options will be showed in the next_screen
      next_screen: test_pagination_in_both_text_options_items

test_pagination_in_both_text_options_items:
  type: menu_screen
  text: |
    This screen has both large text, options, items that exceed ussd text limit
    part of this text would be displayed in the next screen
  items:
    text: "{{item}}"
    value: "{{item}}"
    next_screen: last_screen
    session_key: testing
    with_items:
      - apple
      - boy
      - cat
      - dog
      - egg
      - frog
      - girl
      - house
      - ice
      - joyce
      - kettle
      - lamp
      - mum
      - nurse
      - ostrich
      - pigeon
      - queen
      - river
      - sweet
      - tiger
      - umbrella
      - van
      - water
  options:
    - text: quit_session
      next_screen: last_screen

last_screen:
  type: quit_screen
  text: end of session {{testing}}

test_next_screen_routing_one:
  type: quit_screen
  text: screen_one

test_next_screen_routing_two:
  type: quit_screen
  text: screen_two

9. Function screen ( type -> function_screen )

class ussd.screens.function_screen.FunctionScreen(ussd_request: ussd.core.UssdRequest, handler: str, screen_content: dict, initial_screen: dict, logger=None)[source]

This screen is invisible to the user. Its used to if you want to call a function you have implemented.

Its like a complement of http screen. In http screen you make a request to an external service to perform some logic.

This screen on the contrary if the logic that you want to be executed is within your application you use this screen to execute it.

Your function will be called with UssdRequest object. And it should return a dictionary that will be saved in ussd session

Below is the UssdRequest that will be used.
class UssdRequest(session_id, phone_number, ussd_input, language, default_language=None, use_built_in_session_management=False, expiry=180, **kwargs)
Parameters:
  • session_id

    used to get session or create session if does not exits.

    If session is less than 8 we add s to make the session equal to 8

  • phone_number – This the user identifier
  • input – This ussd input the user has entered.
  • language – Language to use to display ussd
  • kwargs

    Extra arguments. All the extra arguments will be set to the self attribute

    For instance:

    from ussd.core import UssdRequest
    
    ussdRequest = UssdRequest(
        '12345678', '702729654', '1', 'en',
        name='mwas'
    )
    
    # accessing kwarg argument
    ussdRequest.name
    
Screen specification
class ussd.screens.function_screen.FunctionScreenSerializer(instance=None, data=<class 'rest_framework.fields.empty'>, **kwargs)[source]

Fields used to create this screen:

  1. function
    This is the function that will be called at this screen.
  2. session_key
    Once your function has been called the output of your function will be saved in ussd session using session_key
  3. next_screen
    Once your function has been called this it goes to the screen specified in next_screen
Examples of function screens:
initial_screen: get_name

get_name:
  type: function_screen
  next_screen: display_name
  session_key: func_response
  function: ussd.tests.utils.display_name


display_name:
  type: quit_screen
  text: "Your name is {{func_response.name}} and phone number is {{func_response.phone_number}}"

*Once you have created your ussd screens run the following code to validate them:*

python manage.py validate_ussd_journey /path/to/your/ussd/file.yaml