Ussd

Hex.pmHex.pm

Build USSD (Unstructured Supplementary Service Data) applications in Elixir without breaking a sweat.

This README covers installation and a quick example. For every feature in depth - Record, Decisions, Pagination & Truncation, Resuming Sessions, Configurators, Localization, Encrypted Records, Gateway Responses, the mix ussd.graph/ussd.simulate/ussd.lint tasks, Testing, and more - see the full guide.

Installation

Add ussd to your list of dependencies in mix.exs:

def deps do
[
{:ussd, "~> 0.2.0"}
]
end

Features

Usage

Creating states

mix ussd.gen.state Welcome

generates lib/my_app/ussd/states/welcome.ex:

defmodule MyApp.Ussd.States.Welcome do
use Ussd.State
alias Ussd.Decisions.Fallback
transition Fallback.new(), to: __MODULE__
terminate()
@impl Ussd.State
def render(_context) do
Ussd.Menu.build()
|> Ussd.Menu.line("Welcome")
end
end

Building a menu and routing between states

defmodule MyApp.Ussd.States.Welcome do
use Ussd.State, initial: true
alias Ussd.Decisions.Equal
transition Equal.new("1"), to: MyApp.Ussd.States.Airtime
transition Equal.new("2"), to: MyApp.Ussd.States.DataBundle
@impl Ussd.State
def render(_context) do
Ussd.Menu.build()
|> Ussd.Menu.line("Welcome")
|> Ussd.Menu.line("Select an option")
|> Ussd.Menu.listing(["Airtime Topup", "Data Bundle", "TV Subscription", "ECG/GWCL"])
|> Ussd.Menu.line("")
|> Ussd.Menu.text("Powered by Speso")
end
end

Running a request

defmodule MyAppWeb.UssdController do
use MyAppWeb, :controller
def index(conn, params) do
context = Ussd.Context.new(params["session_id"], params["phone"], params["input"] || "")
result =
context
|> Ussd.build()
|> Ussd.use_initial_state(MyApp.Ussd.States.Welcome)
|> Ussd.use_response(&Ussd.Responses.AfricasTalking.respond/3)
|> Ussd.run()
text(conn, result)
end
end

Nothing here is Phoenix-specific — Ussd.run/1 returns whatever your Ussd.Response formatter produces, so it works the same from a Plug, a mix run script, or a test.

Configuration

# config/config.exs
config :ussd,
namespace: "MyApp.Ussd", # used by mix ussd.gen.* and mix ussd.lint's auto-discovery
cache: Ussd.Cache.ETS, # or your own Ussd.Cache implementation
cache_sweep_interval: :timer.minutes(1),
session_ttl: 300, # seconds a mid-flow session survives with no further input
encryption_key: System.fetch_env!("USSD_ENCRYPTION_KEY"), # required for set_encrypted/get_encrypted
gettext_backend: MyApp.Gettext # required for Ussd.Menu.trans/4

Testing

import Ussd.Test
test "buying airtime" do
build(MyApp.Ussd.States.Welcome)
|> start()
|> assert_see("Welcome")
|> input("1")
|> assert_see("Enter amount")
|> input("5")
|> assert_terminated()
end

License

MIT. Please see the license file for more information.