bmatovu/laravel-ussd

Laravel USSD Builder

v1.2.0 2024-04-16 10:54 UTC

This package is auto-updated.

Last update: 2024-04-29 20:11:36 UTC


README

License Unit Tests Code Quality Code Coverage Documentation

Table of Contents

Overview

Effortlessly construct intricate USSD menus with streamlined efficiency by replacing convoluted nests of PHP files with the simplicity of XML-based menu construction. This approach allows for seamless execution similar to standard PHP scripts, minimizing code complexity and enhancing readability.

Let's explore an example of a simple SACCO USSD application.

<?xml version="1.0" encoding="UTF-8"?>
<menu name="sacco"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="menu.xsd">
    <action name="check_user"/>
    <options header="SACCO Services" noback="no">
        <option text="Savings">
            <list header="Saving Accounts" provider="saving_accounts" prefix="account"/>
            <options header="Savings">
                <option text="Deposit">
                    <options header="Deposit From:">
                        <option text="My Number">
                            <variable name="sender" value="{{phone_number}}"/>
                        </option>
                        <option text="Another Number">
                            <question name="sender" text="Enter Phone Number: "/>
                        </option>
                    </options>
                    <question name="amount" text="Enter Amount: "/>
                    <action name="deposit"/>
                </option>
                <option text="Withdraw">
                    <options header="Withdraw To:">
                        <option text="My Number">
                            <variable name="receiver" value="{{phone_number}}"/>
                        </option>
                        <option text="Another Number">
                            <question name="receiver" text="Enter Phone Number: "/>
                        </option>
                    </options>
                    <question name="amount" text="Enter Amount: "/>
                    <action name="withdraw"/>
                </option>
                <option text="Check Balance">
                    <action name="check_balance" text="To see your balance, enter PIN: "/>
                </option>
                <option text="Check Transaction">
                    <question name="transaction_id" text="Enter Transaction ID: "/>
                    <action name="check_transaction" text="To check transaction, enter PIN: "/>
                </option>
            </options>
        </option>
        <option text="Loans">
            <response text="Coming soon."/>
        </option>
    </options>
</menu>

Getting started

Installation

Install the package via the Composer.

composer require bmatovu/laravel-ussd

Configurations

php artisan vendor:publish --provider="Bmatovu\Ussd\UssdServiceProvider" --tag="ussd-config"

Usage

Example

menus/menu.xml

<?xml version="1.0" encoding="UTF-8" ?>
<menu name="demo">
    <question name="guest" text="Enter Name: "/>
    <response text="Hello {{guest}}."/>
</menu>

app/Http/Controller/Api/UssdController

use Bmatovu\Ussd\Ussd;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

/**
 * @see https://developers.africastalking.com/docs/ussd/overview
 */
class UssdController extends Controller
{
    public function __invoke(Request $request): Response
    {
        try {
            $output = Ussd::make('menu.xml', $request->session_id)
                ->handle($request->text);
        } catch(\Exception $ex) {
            return response('END ' . $ex->getMessage());
        }

        return response('CON ' . $output);
    }
}

See more examples in the demo repo

Validation

Publish the menu schema

Defaults to using the schema bundled within the package if none is present in your menus path, usually menus/menu.xsd.

php artisan vendor:publish --provider="Bmatovu\Ussd\UssdServiceProvider" --tag="ussd-schema"

Validate your menu files against the schema

php artisan ussd:validate

VSCode

The RedHat XML package is useful for realtime XSD validations and suggestions.

- <menu name="demo">
+ <menu name="demo"
+     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+     xsi:noNamespaceSchemaLocation="menu.xsd">
      <question name="guest" text="Enter Name: "/>
      <response text="Hello {{guest}}."/>
  </menu>

Simulator

The package comes with a CLI USSD simulator supporting a handful of populator aggregators.

Publish the simulator config file to get started. Update the aggregator and the USSD service endpoint in the config file.

php artisan vendor:publish --provider="Bmatovu\Ussd\UssdServiceProvider" --tag="ussd-simulator"

Usage:

./vendor/bin/ussd --help
./vendor/bin/ussd 256772100103

Aggregators

  • Africastalking
  • Comviva (Airtel & MTN)

If you're an aggregator missing from the current list reachout to have you added. Or simply send a pull request

Constructs

Variable

$color = 'blue';
<variable name="color" value="blue"/>

Note: This tag has no output

Question

$username = readline('Enter username: ');
<question name="username" text="Enter username: "/>

Response

exit('Thank you for banking with us.');
<response text="Thank you for banking with us."/>

Note: this tag throws an exception to mark a break in the normal flow.

Options

Options are like named grouped if, else-if statements that allow a user to navigate to a predefined path.

$choice = readline('Choose service [1. Deposit, 2. Withdraw]: ');

if($choice === 1) {
    // deposit...
} elseif($choice === 2) {
    // withdraw...
}
<options header="Choose service">
    <option text="Deposit">
        <!-- ... -->
    </option>
    <option text="Withdraw">
        <!-- ... -->
    </option>
</options>

Disable backward navigation

By default 0) Back option will be added to the options rendered. Use the attribute noback to disable this behavior.

This behavior may only be used for nested <options> tags.

<options header="Choose service" noback="no">
    <!-- ... -->
</options>

If

Can contain any other tags inclusive of the IF tag itself.

if($role == 'treasurer') {
    // ...
}
<if key="role" value="treasurer">
    <!-- ... -->
</if>

Choose

This construct should cover for if-else, if-elseif-else, and the native switch.

Example #1

if($role == 'treasurer') {
    // ...
} else {
    // ...
}
<choose>
    <when key="role" value="treasurer">
        <!-- ... -->
    </when>
    <otherwise>
        <!-- ... -->
    </otherwise>
</choose>

Example #2

if($role == 'treasurer') {
    // ...
} elseif($role == 'member') {
    // ...
} else {

}
<choose>
    <when key="role" value="treasurer">
        <!-- ... -->
    </when>
    <when key="role" value="member">
        <!-- ... -->
    </when>
    <otherwise>
        <!-- ... -->
    </otherwise>
</choose>

Example #3

switch ($role) {
    case "treasurer":
        // ...
        break;
    case "member":
        // ...
        break;
    default:
        // ...
}
<choose>
    <when key="role" value="treasurer">
        <!-- ... -->
    </when>
    <when key="role" value="memeber">
        <!-- ... -->
    </when>
    <otherwise>
        <!-- ... -->
    </otherwise>
</choose>

Action

Action tags give you the ability to perform more customized operations.

$userInfo = \App\Ussd\Actions\GetUserInfoAction('256732000000');

Arguments

You can pass arguments to actions via attributes or as variables.

<!-- Read from cache -->
<!-- $msisdn = $this->store->get('msisdn'); -->
<action name="get_user_info"/>

<!-- Pass as attribute -->
<action name="get_user_info" msisdn="{{msisdn}}"/>

<!-- Pass as variable -->
<action name="get_user_info">
    <variable name="msisdn" value="{{msisdn}}"/>
</action>

Getting user input

If the text attribute is set on an action, it will behave like the <question> tag waiting for user input

<!-- Approach #1 - user input handled by a qn tag -->
<question name="pin" text="To check balance, enter PIN: "/>
<action name="validate_pin"/>

<!-- Approach #2 - user input handled by the action -->
<action name="validate_pin" text="To check balance, enter PIN: "/>

List

Lists are used to display dynamic items.

The provider must return a list of items, each containing an id and a label.

$listItems = (new \App\Ussd\Providers\SavingAccountsProvider)->load();

[
    [
        'id' => 4364852, // account_id 
        'label' => '01085475262', // account_number
    ],
];
<list header="Saving Accounts" provider="saving_accounts" prefix="account"/>

Accessing the selected item on the list

<!-- Format: {prefix}_<id, label> -->
<response text="{{account_id}}"/><!-- 4364852 -->
<response text="{{account_label}}"/><!-- 01085475262 -->

Note: Similar to actions, you can pass arguments to lists via attributes or as variables.

Advanced

Retries

It's also possible to set the number of retries and a custom error message.

Question

Using regex patterns.

  <question
      name="pin"
      text="Enter PIN: "
+     retries="1"
+     pattern="^[0-9]{5}$"
+     error="You entered the wrong PIN. Try again" />

Options & Lists

Validation is against the possible list options.

  <options
      header="Choose a test"
+     retries="1"
+     error="Choose the correct number:">
      ...
  </option>
  <list 
      header="Saving Accounts" 
      provider="saving_accounts" 
      prefix="account" 
+     retries="1"
+     error="Choose the correct number:"/>

Note: Retries in <action> tags are discouraged because the action tags are not aware of tags preceeding them.

Comparisons

The <if> and <when> tags allow comparisions.

Falls back to eq if the cond is not set or it's unsupported.

<if key="age" value="18">
<if key="age" cond="eq" value="18">
Type Conditions
Numbers - lt
- gt
- lte
- gte
- eq
- ne
- btn
Strings - str.equals
- str.not_equals
- str.starts
- str.ends
- str.contains
Regex - regex.match
Arrays - arr.in
- arr.not_in
Dates - date.equals
- date.before
- date.after
- date.between
Time - time.equals
- time.before
- time.after
- time.between

Localization

Create the translation files in your project and return keys in your menu files... See the example below

menus/menu.xml

<menu name="demo">
    <action name="set_locale" locale="fr" />
    <question name="guest" text="AskForName" />
    <response text="GreetGuest" />
</menu>

resources/lang/fr.json

{
    "AskForName": "Entrez le nom:",
    "GreetGuest": "Boujour {{guest}}"
}

USSD simulation

ussd-demo$ vendor/bin/ussd 250723000123
Entrez le nom: 
John

Boujour John

Note:

  • use the set_locale action to change locale directly from the ussd menu, and
  • use App::setLocale to change locale in your controller

Cache

This package persists USSD session data in cache. Each key is prefixed with the session_id and it automatically expires after the configured ttl.

Accessing variables

<variable name="color" value="blue"/>
$this->store->get('color'); // blue

Cache::store($driver)->get("{$sessionId}color"); // blue

Reusing existing variables

<variable name="msg" value="Bye bye."/>

<response text="{{msg}}"/> <!-- Bye bye -->

Parser

Save default variables

Example for saving any variable from the incoming USSD request.

Ussd::make($menu, $request->session_id)
    ->save([
        'phone_number' => $request->phone_number,
    ])
    ->handle(...);

Use custom menu entry point

By default the parsing starts at the 1st element in your menu file, i.e /menu/*[1].

If you wish to start from a different point or use a custom menu file structure. Here's how to go about it...

Ussd::make($menu, $request->session_id)
    ->entry("/menus/menu[@name='sacco']/*[1]")
    ->handle(...);

See: xpath playground

Simulation

You can extend the USSD simulator with your aggregator of choice by simply registering it in the simulator config file.

The provider class should implement Bmatovu\Ussd\Contracts\Aggregator.

simulator.json

  {
+     "aggregator": "africastalking",
      "aggregators": {
+         "hubtel": {
+             "provider": "App\\Ussd\\Simulator\\Africastalking",
+             "uri": "http://localhost:8000/api/ussd/africastalking",
+             "service_code": "*123#"
+         }
      }
  }

JSON

Why use XML 🥴 and not JSON 😉?

XML is better suited for writing constructs resembling programming languages. It offers straightforward validation of schemas. Additionally, XML is both more compact and readable.

Compare the snippets below...

<menu name="demo">
    <question name="guest" text="Enter Name: "/>
    <response text="Hello {{guest}}."/>
</menu>
{
    "@name": "demo",
    "question": {
        "@name": "guest",
        "@text": "Enter Name:"
    },
    "response": {
        "@text": "Hello {{guest}}."
    }
}

Testing

To run the package's unit tests, run the following command:

composer test

Security

If you find any security related issues, please contact me directly at mtvbrianking@gmail.com to report it.

Contribution

If you wish to make any changes or improvements to the package, feel free to make a pull request.

Note: A contribution guide will be added soon.

Alternatives

License

The MIT License (MIT). Please see License file for more information.