Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Ussd

Hex.pm Hex.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

  • Menus as modules — define screens as Ussd.State modules with a fluent Ussd.Menu builder, and route between them declaratively with transition/2.
  • Built-in decisions — match input with Equal, Between, In, Regex, IsNumeric and more out of the box; scaffold custom ones with mix ussd.gen.decision.
  • Conditional branchingUssd.Action modules decide the next state at runtime (e.g. after an HTTP call), for flows that can't be expressed as static transitions.
  • Back navigationback/2 with an automatic per-session history stack, no manual bookkeeping.
  • Automatic paginationuse Ussd.Pagination plus paginate/1 page long listings without manual bookkeeping.
  • Response truncationtruncate/1 caps how many characters a screen returns, so dynamic content can't blow past your gateway's character limit.
  • Resumable sessionsuse_continuing_state/4 lets a redial pick back up where a timed-out session left off, silently or after confirming with the user.
  • Configurators — group and share repeated setup (response format, exception handling, etc.) across entry points.
  • Localized menus — build menu content from a Gettext backend, with locale persisted across a session.
  • Session records with real expiry — a Ussd.Record API (get/set/increment/ decrement/...) for persisting data during a session, backed by a pluggable Ussd.Cache with actual TTL, swept periodically instead of leaking forever.
  • Encrypted session data — store sensitive values (PINs, account numbers) encrypted at rest via set_encrypted/get_encrypted.
  • Built-in gateway responses — ships Ussd.Response formatters for Speso, Africa's Talking, Nsano, Nalo, Moolre and Arkesel; scaffold your own with mix ussd.gen.response.
  • Exception handling — implement Ussd.ExceptionHandler to turn an unhandled exception into a message the caller sees, instead of a dead session.
  • Flow visualizationmix ussd.graph renders a Mermaid state diagram of a flow from its transition/2/back/2/terminate/0 declarations.
  • Interactive simulationmix ussd.simulate lets you walk a flow in the terminal like a real handset, no gateway or phone required.
  • Flow lintingmix ussd.lint catches dead ends, broken transitions, duplicate matches and unreachable states before they ship.
  • Testing utilities — a fluent Ussd.Test API for asserting screens, context and session state across multi-step conversations.
  • Session events:telemetry events for state entry and session termination, for logging or analytics without touching core modules.
  • Mix generators — scaffold states, actions, responses, decisions, configurators and exception handlers with mix ussd.gen.*.

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.