Skip to content

Commit

Permalink
fix: metadata internal implementation (#38)
Browse files Browse the repository at this point in the history
* chore: improve metadata internals

* chore: fix failing text

* chore: fix routing

* chore: update readme

* chore: update readme
  • Loading branch information
lenileiro authored Oct 9, 2021
1 parent 0d893b6 commit e6cdf15
Show file tree
Hide file tree
Showing 9 changed files with 120 additions and 52 deletions.
11 changes: 10 additions & 1 deletion .iex.exs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ defmodule HomeResolver do
|> ExUssd.set(resolve: &business_account/2)
end

def home(menu, _payload) do
def ussd_init(menu, _payload) do
data = %{user_name: "john_doe", account_type: :personal}
menu
|> ExUssd.set(title: "Welcome")
Expand Down Expand Up @@ -55,6 +55,15 @@ defmodule HomeResolver do
menu
|> ExUssd.set(title: "You have Entered the Secret Number, 5555")
|> ExUssd.set(should_close: true)
else
menu
|> ExUssd.set(error: "You have Entered the Wrong Number")
end
end

def ussd_after_callback(%{error: true} = menu, _payload, %{attempt: %{count: 3}}) do
menu
|> ExUssd.set(title: "Account is locked, Dial *234# to reset your account")
|> ExUssd.set(should_close: true)
end
end
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## v.1.0.1 - 2021-10-02

- Metadata internal bug fix [#38](https://github.com/beamkenya/ex_ussd/pull/38)

## v1.0.0 - 2021-10-02

- Add support for Zero Based menu list
21 changes: 4 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,11 @@ Goals:
- Detailed error messages and documentation.
- A focus on robustness and production-level performance.

## Table of contents

- [Why Use ExUssd](#why-use-exussd)
- [Documentation](#documentation)
- [Installation](#installation)
- [Configuration](#configuration)
- [Usage](#usage)
- [Contribution](#contribution)
- [Contributors](#contributors)
- [Licence](#licence)

## Why Use ExUssd?

ExUssd lets you create simple, flexible, and customizable USSD interface.
Under the hood ExUssd uses Elixir Registry to create and route individual USSD session.

https://user-images.githubusercontent.com/23293150/124460086-95ebf080-dd97-11eb-87ab-605f06291563.mp4

## Documentation

The docs can be found at [https://hexdocs.pm/ex_ussd](https://hexdocs.pm/ex_ussd)
Expand All @@ -39,7 +26,7 @@ by adding `ex_ussd` to your list of dependencies in `mix.exs`:
```elixir
defp deps do
[
{:ex_ussd, "~> 1.0.0"}
{:ex_ussd, "~> 1.0.1"}
]
end
```
Expand Down Expand Up @@ -78,15 +65,15 @@ defmodule ApiWeb.HomeResolver do
ExUssd.set(menu, title: "Enter your PIN")
end

def ussd_callback(menu, payload, %{attempt: attempt}) do
def ussd_callback(menu, payload,%{attempt: %{count: count}}) do
if payload.text == "5555" do
ExUssd.set(menu, resolve: &success_menu/2)
else
ExUssd.set(menu, error: "Wrong PIN, #{3 - attempt} attempt left\n")
ExUssd.set(menu, error: "Wrong PIN, #{2 - count} attempt left\n")
end
end

def ussd_after_callback(%{error: true} = menu, _payload, %{attempt: 3}) do
def ussd_after_callback(%{error: true} = menu, _payload, %{attempt: %{count: 3}}) do
menu
|> ExUssd.set(title: "Account is locked, Dial *234# to reset your account")
|> ExUssd.set(should_close: true)
Expand Down
18 changes: 12 additions & 6 deletions lib/ex_ussd.ex
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ defmodule ExUssd do
Example:
```elixir
%{attempt: 1, invoked_at: ~U[2024-09-25 09:10:15Z], route: "*555*1#", text: "1"}
%{attempt: %{count: 2, input: ["wrong2", "wrong1"]}, invoked_at: ~U[2024-09-25 09:10:15Z], route: "*555*1#", text: "1"}
```
"""
@type metadata() :: map()
Expand Down Expand Up @@ -257,7 +257,7 @@ defmodule ExUssd do
...> ExUssd.set(menu, error: "Wrong PIN\\n")
...> end
...> end
...> def ussd_after_callback(%{error: true} = menu, _payload, %{attempt: 3}) do
...> def ussd_after_callback(%{error: true} = menu, _payload, %{attempt: %{count: 3}}) do
...> menu
...> |> ExUssd.set(title: "Account is locked, you have entered the wrong PIN 3 times")
...> |> ExUssd.set(should_close: true)
Expand All @@ -270,7 +270,7 @@ defmodule ExUssd do
...> end
iex> # To simulate a user entering wrong PIN 3 times.
iex> menu = ExUssd.new(name: "PIN", resolve: AppWeb.HomeResolver)
iex> ExUssd.to_string!(menu, :ussd_after_callback, payload: %{text: "5556", attempt: 3})
iex> ExUssd.to_string!(menu, :ussd_after_callback, payload: %{text: "5556", attempt: %{count: 3}})
"Account is locked, you have entered the wrong PIN 3 times"
"""
@callback ussd_after_callback(
Expand Down Expand Up @@ -432,13 +432,15 @@ defmodule ExUssd do
iex> resolve = fn menu, _payload ->
...> menu
...> |> ExUssd.set(title: "Menu title")
...> |> ExUssd.add(ExUssd.new(name: "offers", resolve: &(ExUssd.set(&1, title: "offers"))))
...> |> ExUssd.add(ExUssd.new(name: "offers", resolve: fn menu, _ -> ExUssd.set(menu, title: "offers") end))
...> |> ExUssd.add(ExUssd.new(name: "option 1", resolve: &(ExUssd.set(&1, title: "option 1"))))
...> |> ExUssd.add(ExUssd.new(name: "option 2", resolve: &(ExUssd.set(&1, title: "option 2"))))
...> end
iex> menu = ExUssd.new(name: "HOME", is_zero_based: true, resolve: resolve)
iex> ExUssd.to_string!(menu, [])
"Menu title\\n0:offers\\n1:option 1\\n2:option 2"
iex> ExUssd.to_string!(menu, [simulate: true, payload: %{text: "0"}])
"offers"
NOTE:
`ExUssd.new/1` can be used to create a menu with a callback function.
Expand Down Expand Up @@ -495,13 +497,17 @@ defmodule ExUssd do
...> def product_b(menu, _payload), do: menu |> ExUssd.set(title: "selected product b")
...> def product_c(menu, _payload), do: menu |> ExUssd.set(title: "selected product c")
...> def account(%{data: %{type: :personal, name: name}} = menu, _payload) do
...> # Get Personal account details, then set as data
...> # Should be stateless, don't put call functions with side effect (Insert to DB, fetch)
...> # Because it will be called every time the menu is rendered because the menu `:name` is dynamic
...> # See `ExUssd.new/2` for more details where `:name` is static.
...> menu
...> |> ExUssd.set(name: "Personal account")
...> |> ExUssd.set(resolve: &(ExUssd.set(&1, title: "Personal account")))
...> end
...> def account(%{data: %{type: :business, name: name}} = menu, _payload) do
...> # Get Business account details, then set as data
...> # Should be stateless, don't put call functions with side effect (Insert to DB, fetch)
...> # Because it will be called every time the menu is rendered because the menu `:name` is dynamic
...> # See `ExUssd.new/2` for more details where `:name` is static.
...> menu
...> |> ExUssd.set(name: "Business account")
...> |> ExUssd.set(resolve: &(ExUssd.set(&1, title: "Business account")))
Expand Down
47 changes: 30 additions & 17 deletions lib/ex_ussd/executer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@ defmodule ExUssd.Executer do
when not is_nil(navigate) do
menu
|> Map.put(:resolve, navigate)
|> get_next_menu(payload, opts)
|> get_next_menu(menu, payload, Keyword.merge(opts, navigate: true))
end

def execute_callback(%ExUssd{resolve: resolve} = menu, payload, opts)
def execute_callback(%ExUssd{resolve: resolve, menu_list: menu_list} = menu, payload, opts)
when is_atom(resolve) do
if function_exported?(resolve, :ussd_callback, 3) do
metadata =
Expand All @@ -87,20 +87,30 @@ defmodule ExUssd.Executer do
%{
route: "*test#",
invoked_at: DateTime.truncate(DateTime.utc_now(), :second),
attempt: 1
attempt: %{count: 1}
},
payload
)
)

try do
with %ExUssd{error: error} = current_menu <-
apply(resolve, :ussd_callback, [%{menu | resolve: nil}, payload, metadata]) do
apply(resolve, :ussd_callback, [
%{menu | resolve: nil, menu_list: []},
payload,
metadata
]) do
if is_bitstring(error) do
build_response_menu(:halt, current_menu, menu, payload, opts)
if Keyword.get(opts, :state) do
ExUssd.Registry.add_attempt(payload[:session_id], payload[:text])
end

if Enum.empty?(menu_list) do
build_response_menu(:halt, current_menu, menu, payload, opts)
end
else
build_response_menu(:ok, current_menu, menu, payload, opts)
|> get_next_menu(payload, opts)
|> get_next_menu(menu, payload, opts)
end
end
rescue
Expand Down Expand Up @@ -144,7 +154,7 @@ defmodule ExUssd.Executer do
%{
route: "*test#",
invoked_at: DateTime.truncate(DateTime.utc_now(), :second),
attempt: 3
attempt: %{count: 3}
},
payload
)
Expand All @@ -153,15 +163,15 @@ defmodule ExUssd.Executer do
try do
with %ExUssd{error: error} = current_menu <-
apply(resolve, :ussd_after_callback, [
%{menu | resolve: nil, error: error_state},
%{menu | resolve: nil, menu_list: [], error: error_state},
payload,
metadata
]) do
if is_bitstring(error) do
build_response_menu(:halt, current_menu, menu, payload, opts)
else
build_response_menu(:ok, current_menu, menu, payload, opts)
|> get_next_menu(payload, opts)
|> get_next_menu(menu, payload, opts)
end
end
rescue
Expand Down Expand Up @@ -189,14 +199,16 @@ defmodule ExUssd.Executer do
%{route: route} = ExUssd.Route.get_route(payload)
%{session_id: session} = payload
ExUssd.Registry.add_route(session, route)
end

{:ok, %{current_menu | parent: fn -> menu end}}
{:ok, %{current_menu | parent: fn -> menu end}}
else
{:ok, current_menu}
end
end

defp get_next_menu(menu, payload, opts) do
defp get_next_menu(menu, parent, payload, opts) do
fun = fn
%ExUssd{orientation: orientation, data: data, resolve: resolve} ->
%ExUssd{orientation: orientation, data: data, resolve: resolve} when not is_nil(resolve) ->
new_menu =
ExUssd.new(
orientation: orientation,
Expand All @@ -207,7 +219,11 @@ defmodule ExUssd.Executer do

current_menu = execute_init_callback!(new_menu, payload)

build_response_menu(:ok, current_menu, menu, payload, opts)
if Keyword.get(opts, :navigate) do
build_response_menu(:ok, current_menu, menu, payload, opts)
else
{:ok, %{current_menu | parent: fn -> parent end}}
end

response ->
response
Expand All @@ -218,9 +234,6 @@ defmodule ExUssd.Executer do
{:ok, %ExUssd{resolve: resolve} = menu} when not is_nil(resolve) ->
menu

%ExUssd{} = menu ->
menu

menu ->
menu
end
Expand Down
9 changes: 1 addition & 8 deletions lib/ex_ussd/navigation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,9 @@ defmodule ExUssd.Navigation do
@spec get_menu(integer(), map(), ExUssd.t(), map()) :: {:ok | :halt, ExUssd.t()}
defp get_menu(pos, route, menu, payload)

defp get_menu(
_pos,
route,
%ExUssd{default_error: error, menu_list: []} = menu,
%{session_id: session} = payload
) do
defp get_menu(_pos, _route, %ExUssd{default_error: error, menu_list: []} = menu, payload) do
with response when not is_menu(response) <-
Executer.execute_callback(menu, payload) do
Registry.add_attempt(session, route[:text])
{:halt, %{menu | error: error}}
end
end
Expand Down Expand Up @@ -179,7 +173,6 @@ defmodule ExUssd.Navigation do
{:ok, %{current_menu | parent: fn -> parent_menu end}}

nil ->
Registry.add_attempt(session, route[:text])
{:halt, %{menu | error: default_error}}
end
end
Expand Down
6 changes: 4 additions & 2 deletions lib/ex_ussd/utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ defmodule ExUssd.Utils do
%{attempt: attempt, invoked_at: invoked_at, route: routes_string, text: text}
end

def get_menu(%ExUssd{} = menu, opts) do
def get_menu(%ExUssd{is_zero_based: is_zero_based} = menu, opts) do
payload = Keyword.get(opts, :payload, %{text: "set_init_text"})

position =
Expand All @@ -135,7 +135,9 @@ defmodule ExUssd.Utils do
current_menu = get_menu(menu, :ussd_callback, opts)

if error do
case Enum.at(Enum.reverse(menu_list), position - 1) do
from = if(is_zero_based, do: 0, else: 1)

case Enum.at(Enum.reverse(menu_list), position - from) do
nil ->
get_menu(%{menu | error: true}, :ussd_after_callback, opts)

Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule ExUssd.MixProject do
use Mix.Project

@source_url "https://github.com/beamkenya/ex_ussd.git"
@version "1.0.0"
@version "1.0.1"

def project do
[
Expand Down
54 changes: 54 additions & 0 deletions test/ex_ussd/op_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -289,4 +289,58 @@ defmodule ExUssd.OpTest do
})
end
end

describe "metadata" do
defmodule PinResolver do
use ExUssd

def ussd_init(menu, _) do
ExUssd.set(menu, title: "Enter your PIN")
end

def ussd_callback(menu, payload, %{attempt: %{count: count}}) do
if payload.text == "5555" do
ExUssd.set(menu, resolve: &success_menu/2)
else
ExUssd.set(menu, error: "Wrong PIN, #{2 - count} attempt left\n")
end
end

def ussd_after_callback(%{error: true} = menu, _payload, %{attempt: %{count: 3}}) do
menu
|> ExUssd.set(title: "Account is locked, Dial *234# to reset your account")
|> ExUssd.set(should_close: true)
end

def success_menu(menu, _) do
menu
|> ExUssd.set(title: "You have Entered the Secret Number, 5555")
|> ExUssd.set(should_close: true)
end
end

setup do
%{
menu: ExUssd.new(name: Faker.Company.name(), resolve: PinResolver),
session: "#{System.unique_integer()}"
}
end

test "successfully navigates to the first menu", %{menu: menu, session: session} do
assert {:ok, %{menu_string: "Enter your PIN", should_close: false}} ==
ExUssd.goto(%{
payload: %{session_id: session, text: "*444#", service_code: "*444#"},
menu: menu
})
end

test "successfully render the first error message", %{menu: menu, session: session} do
assert {:ok,
%{menu_string: "Wrong PIN, 2 attempt left\nEnter your PIN", should_close: false}} ==
ExUssd.goto(%{
payload: %{session_id: session, text: "2211", service_code: "*444#"},
menu: menu
})
end
end
end

0 comments on commit e6cdf15

Please sign in to comment.