Ussd Airflow Setup and Usage Guide
Ussd Airflow Setup and Usage Guide
Release 0.0
Francis Mwangi
1 Getting Started 3
1.1 Setup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2 Creating ussd screens . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
3 Internal Architecture 31
i
ii
Ussd Airflow Documentation, Release 0.0
Ussd Airflow is a platform used to create ussd application by defining ussd screens in a yaml file
Contents 1
Ussd Airflow Documentation, Release 0.0
2 Contents
CHAPTER 1
Getting Started
Getting started with ussd airflow. This assumes you know how ussd works, to learn more how ussd works How ussd
works
Setup
INSTALLED_APPS = [
'ussd.apps.UssdConfig',
]
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer
˓→'
urlpatterns = [
url(r'^africastalking_gateway',
AfricasTalkingUssdGateway.as_view(),
name='africastalking_url')
]
3
Ussd Airflow Documentation, Release 0.0
To use the existing Africastalking ussd gateway and your own ussd screen. Create a yaml file. On the
yaml create your ussd screen. Learn more on how to create ussd screen here Creating ussd screens.
For quick start copy the below yaml
initial_screen: enter_name
enter_name:
type: input_screen
text: Enter your name
input_identifier: name
next_screen: enter_age
enter_age:
type: input_screen
text: Enter your age
input_identifier: age
next_screen: show_details
show_details:
type: quit_screen
text: You have entered name as {{name}} and age as {{age}}
Next step add this to your settings. For ussd airflow to know where your ussd screens files are located.
DEFAULT_USSD_SCREEN_JOURNEY = "/file/path/of/the/screen"
* Define Http method either get or post (Mandatory) The http method should return
Ussd Request
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.
ussdRequest = UssdRequest(
'12345678', '702729654', '1', 'en',
name='mwas'
)
* define this varialbe customer_journey_conf This is the path of the file that has ussd
screens If you want your file to be dynamic implement the following method
get_customer_journey_conf it will be called by request object
* define this variable customer_journey_namespace Ussd_airflow uses this names-
pace to save the customer journey content in memory. If you want
customer_journey_namespace to be dynamic implement this method
get_customer_journey_namespace it will be called with request object
* override HttpResponse In ussd airflow the http method return UssdRequest object not
Http response. Then ussd view gets UssdResponse object and convert it to HttpRe-
sponse. The default HttpResponse returned is a normal HttpResponse with body
being ussd text
To override HttpResponse returned define this method. ussd_response_handler it
will be called with UssdResponse object.
class ussd.core.UssdResponse(text, status=True, ses-
sion=None)
Parameters
· text – This is the ussd text to display to the user
· status – This shows the status of ussd session.
True -> to continue with the session
False -> to end the session
· session – This is the session object of the ussd ses-
sion
Example of Ussd view
class SampleOne(UssdView):
1.1. Setup 5
Ussd Airflow Documentation, Release 0.0
class SampleOne(UssdView):
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
– 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 dis-
played 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
options:
- text: option one
next_screen: screen_one
- text: option two
next_screen: screen_two
•items: Unlike options where each option has its own screen to redirect in items we have a list of items to
display and regardless of the input user will be redirected to one screen.
Example of items
menu_screen_with_item_example:
type: menu_screen
text: choose one item
items:
text: "{{key}} for {{value}}"
value: "{{item}}"
next_screen: display_option
session_key: testing
with_dict:
a: apple
b: boy
c: cat
If the user selects “2”, that would be translated by the value key, it will result to “b”, then “b”
will be saved with session_key provided and the user will be directed to the next screen which is
display_option.
To reference the selected item, use {{your_session_key}}
•error_message: (optional) This is message to display if the user enter the wrong value.
defaults to “Please enter a valid choice.”
•option and items are mutual exclusive.
Example:
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
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
test_pagination_in_both_text_options_items:
type: menu_screen
text: |
This screen has both large text, options, items that exceed ussd text
˓→limit
- 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
initial_screen: example_of_quit_screen
example_of_quit_screen:
type: quit_screen
text: "Test getting variable from os environmen. {{TEST_VARIABLE}}"
(c) 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}}.
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}}
class ussd.screens.update_session_screen.UpdateSessionScreen(ussd_request:
ussd.core.UssdRequest,
handler: str,
screen_content:
dict, initial_screen:
dict, logger=None)
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
(a) key the key to be used to save
(b) value the value to store with the key above
(c) 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}}
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")
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"
return self.ussd_request.forward(
self.screen_content['next_screen']
)
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
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
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
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
ussdRequest = UssdRequest(
'12345678', '702729654', '1', 'en',
name='mwas'
)
Screen specification
class ussd.screens.function_screen.FunctionScreenSerializer(instance=None,
data=<class
‘rest_framework.fields.empty’>,
**kwargs)
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: enter_fisrt_number
enter_fisrt_number:
type: input_screen
input_identifier: first_number
next_screen: enter_second_number
text: Enter your first number
enter_second_number:
type: input_screen
input_identifier: second_number
next_screen: sum_numbers
text: Enter your second number
sum_numbers:
type: function_screen
default_next_screen: display_odd_number
function: ussd.tests.utils.sum_numbers
session_key: sum_results
next_screen:
- condition: sum_results|int % 2 == 0
next_screen: display_even_number
display_odd_number:
type: quit_screen
text: "The results was an odd number which is {{sum_results}}"
display_even_number:
type: quit_screen
text: "The results was an even number which is {{sum_results}}"
*Once you have created your ussd screens run the following code to validate them:*
Unstructured Supplementary Service Data (USSD) is a protocol used by GSM cellphones to communicate with their
service provider’s computers. USSD can be used for WAP browsing, prepaid callback service, mobile money services,
location-based content services, menu-based information services, or even as part of configuring the phone on the
network.
From the diagram above, a request is sent from a mobile phone to a telecom network such Vodafone.
The Ussd Gateway (telecom) then sends the request to your ussd application (i.e where we have the business logic
which determines the menu to serve the use on receiving user’s request.)
Your ussd application then responds to the request, and Ussd gateway goes ahead and displays your content to the user
Below is a another diagram to help understand the concept
27
Ussd Airflow Documentation, Release 0.0
Before I explain why we need Ussd Airflow lets first look at one example of ussd user case
One could decide to develop a mobile-initiated “Balance Enquiry and Top Up” application using USSD signaling,
enabling a mobile user to interact with an application via the user’s handset, in order to view his/her current mobile
account balance and top up as needed.
An example of such an application could be as follows:
1. A mobile user initiates the “Balance Enquiry and Top Up” service by dialing the USSD string defined by the
service provider; for example, *#123#.
2. TheUSSD application receives the service request from the user and responds by sending the user a menu of
options.
3. The user responds by selecting a “current balance” option.
4. The USSD application sends back details of the mobile user’s current account balance and also gives the option
to top up the balance.
5. The user selects to top up his/her account.
6. The application responds by asking how much credit to add?
7. The mobile user responds with the amount to add.
8. The USSD application responds by sending an updated balance and ends the session.
The figure below shows an example of the MAP/TCAP message sequence required to realize the data transfers between
a mobile user’s handset and the USSD application to implement the “Balance Enquiry and Top Up” service described
above.
As you have seen in the previous section your ussd application is responsible for the content displayed.
Suppose you want to change the wordings in the ussd screen you are displaying to the user,what is involved in most
cases or rather all cases is you make a change in your code and deploy, that’s peanuts for most developers
The problem is once you start having many ussd screens and multiple ussd application and many requirements of
changing ussd screen, the task that was peanuts becomes overwhelming and would probably start thinking of a way
the Product owners would change the ussd content without you being involved and thats where ussd aiflow comes in,
providing an interface for users to change ussd workflows without code change
Internal Architecture
Comming soon
31
Ussd Airflow Documentation, Release 0.0
u
ussd.core, 31
ussd.screens.initial_screen, 6
ussd.screens.input_screen, 7
33
Index
C
CustomScreen (class in ussd.screens.custom_screen), 19
F
FunctionScreen (class in ussd.screens.function_screen),
24
FunctionScreen.UssdRequest (class in ussd.core), 24
FunctionScreenSerializer (class in
ussd.screens.function_screen), 24
H
HttpScreen (class in ussd.screens.http_screen), 14
I
InitialScreen (class in ussd.screens.initial_screen), 6
InputScreen (class in ussd.screens.input_screen), 7
M
MenuScreen (class in ussd.screens.menu_screen), 9
Q
QuitScreen (class in ussd.screens.quit_screen), 14
R
RouterScreen (class in ussd.screens.router_screen), 16
U
UpdateSessionScreen (class in
ussd.screens.update_session_screen), 18
ussd.core (module), 31
ussd.screens.initial_screen (module), 6
ussd.screens.input_screen (module), 7
UssdResponse (class in ussd.core), 5
UssdView (class in ussd.core), 4
UssdView.UssdRequest (class in ussd.core), 4
34