diff --git a/lib/at_ex.ex b/lib/at_ex.ex index 9852761..0568fa5 100644 --- a/lib/at_ex.ex +++ b/lib/at_ex.ex @@ -36,21 +36,21 @@ defmodule AtEx do ## Examples - iex> AtEx.send_airtime(%{recipients: [%{phone_number: "+254721978097", amount: "KES 50"}]}) - {:ok, - %{ - "errorMessage" => "None", - "numSent" => 1, - "responses" => [ - %{ - "amount" => "KES 50.0000", - "discount" => "KES 2.0000", - "errorMessage" => "None", - "phoneNumber" => "+254721978097", - "requestId" => "ATQid_630557e624c70f2b0d2c5e90ebc282bb", - "status" => "Sent" - } - ], + iex> AtEx.send_airtime(%{recipients: [%{phone_number: "+254721978097", amount: "KES 50"}]}) + {:ok, + %{ + "errorMessage" => "None", + "numSent" => 1, + "responses" => [ + %{ + "amount" => "KES 50.0000", + "discount" => "KES 2.0000", + "errorMessage" => "None", + "phoneNumber" => "+254721978097", + "requestId" => "ATQid_630557e624c70f2b0d2c5e90ebc282bb", + "status" => "Sent" + } + ], "totalAmount" => "ZAR 7.0277", "totalDiscount" => "ZAR 0.2811" }} @@ -76,28 +76,28 @@ defmodule AtEx do sent ## Parameters - - map: a map containing a `to` and `message` key optionally it may also contain `from`, bulk_sms, enqueue, key_word - link_id and retry_hours keys, see the docs at https://build.at-labs.io/docs/sms%2Fsending for how to use these keys + - map: a map containing a `to` and `message` key optionally it may also contain `from`, bulk_sms, enqueue, key_word + link_id and retry_hours keys, see the docs at https://build.at-labs.io/docs/sms%2Fsending for how to use these keys ## Examples - iex> AtEx.send_sms(%{to: "+254721978097", message: "Howdy"}) - {:ok, + iex> AtEx.send_sms(%{to: "+254721978097", message: "Howdy"}) + {:ok, + %{ + "SMSMessageData" => %{ + "Message" => "Sent to 1/1 Total Cost: ZAR 0.1124", + "Recipients" => [ %{ - "SMSMessageData" => %{ - "Message" => "Sent to 1/1 Total Cost: ZAR 0.1124", - "Recipients" => [ - %{ - "cost" => "KES 0.8000", - "messageId" => "ATXid_96e52a761a82c1bad58e885109224aad", - "number" => "+254721978097", - "status" => "Success", - "statusCode" => 101 - } - ] + "cost" => "KES 0.8000", + "messageId" => "ATXid_96e52a761a82c1bad58e885109224aad", + "number" => "+254721978097", + "status" => "Success", + "statusCode" => 101 } - }} + ] + } + }} """ - defdelegate send_sms(map), to: Sms + defdelegate send_sms(map), to: Sms.Bulk @doc """ @@ -120,5 +120,5 @@ defmodule AtEx do """ - defdelegate fetch_sms(map), to: Sms + defdelegate fetch_sms(map), to: Sms.Bulk end diff --git a/lib/at_ex/gateway/Sms/bulk.ex b/lib/at_ex/gateway/Sms/bulk.ex new file mode 100644 index 0000000..8c0cd8a --- /dev/null +++ b/lib/at_ex/gateway/Sms/bulk.ex @@ -0,0 +1,58 @@ +defmodule AtEx.Gateway.Sms.Bulk do + @moduledoc """ + This module holds the implementation for the HTTP Gateway that runs calls against the Africas Talking API + SMS endpoint, use it to POST and GET requests to the SMS endpoint + """ + use AtEx.Gateway.Base, url: "https://api.sandbox.africastalking.com/version1" + + @doc """ + This function builds and runs a post request to send an SMS via the Africa's talking SMS endpoint, this + function accepts a map of parameters that should always contain the `to` address and the `message` to be + sent + + ## Parameters + attrs: - a map containing a `to` and `message` key optionally it may also contain `from`, bulk_sms, enqueue, key_word + link_id and retry_hours keys, see the docs at https://build.at-labs.io/docs/sms%2Fsending for how to use these keys + """ + @spec send_sms(map()) :: {:ok, term()} | {:error, term()} + def send_sms(attrs) do + username = Application.get_env(:at_ex, :username) + + params = + attrs + |> Map.put(:username, username) + + "/messaging" + |> post(params) + |> process_result() + end + + @doc """ + This function makes a get request to fetch an SMS via the Africa's talking SMS endpoint, this + function accepts a map of parameters that optionally accepts `lastReceivedId` of the message. + sent + + ## Parameters + attrs: - an empty map or a map containing optionally `lastReceivedId` of the message to be fetched, see the docs at https://build.at-labs.io/docs/sms%2Ffetch_messages for how to use these keys + """ + + @spec fetch_sms(map()) :: {:error, any()} | {:ok, any()} + def fetch_sms(attrs) do + username = Application.get_env(:at_ex, :username) + + params = + attrs + |> Map.put(:username, username) + |> Map.to_list() + + with {:ok, %{status: 200} = res} <- get("/messaging", query: params) do + {:ok, Jason.decode!(res.body)} + else + {:ok, val} -> + {:error, %{status: val.status, message: val.body}} + + {:error, message} -> + {:error, message} + end + end +end diff --git a/lib/at_ex/gateway/sms.ex b/lib/at_ex/gateway/Sms/premium.ex similarity index 66% rename from lib/at_ex/gateway/sms.ex rename to lib/at_ex/gateway/Sms/premium.ex index f3b63f7..f91a6c5 100644 --- a/lib/at_ex/gateway/sms.ex +++ b/lib/at_ex/gateway/Sms/premium.ex @@ -1,72 +1,10 @@ -defmodule AtEx.Gateway.Sms do - @moduledoc """ - This module holds the implementation for the HTTP Gateway that runs calls against the Africas Talking API - SMS endpoint, use it to POST and GET requests to the SMS endpoint - """ +defmodule AtEx.Gateway.Sms.PremiumSubscriptions do use AtEx.Gateway.Base, url: "https://api.sandbox.africastalking.com/version1" - - @doc """ - This function builds and runs a post request to send an SMS via the Africa's talking SMS endpoint, this - function accepts a map of parameters that should always contain the `to` address and the `message` to be - sent - - ## Parameters - attrs: - a map containing a `to` and `message` key optionally it may also contain `from`, bulk_sms, enqueue, key_word - link_id and retry_hours keys, see the docs at https://build.at-labs.io/docs/sms%2Fsending for how to use these keys - """ - @spec send_sms(map()) :: {:ok, term()} | {:error, term()} - def send_sms(attrs) do - username = Application.get_env(:at_ex, :username) - - params = - attrs - |> Map.put(:username, username) - - with {:ok, %{status: 201} = res} <- post("/messaging", params) do - {:ok, Jason.decode!(res.body)} - else - {:ok, val} -> - {:error, %{status: val.status, message: val.body}} - - {:error, message} -> - {:error, message} - end - end - - @doc """ - This function makes a get request to fetch an SMS via the Africa's talking SMS endpoint, this - function accepts an map of parameters that optionally accepts `lastReceivedId` of the message. - sent - - ## Parameters - attrs: - an empty map or a map containing optionally `lastReceivedId` of the message to be fetched, see the docs at https://build.at-labs.io/docs/sms%2Ffetch_messages for how to use these keys - """ - - @spec fetch_sms(map()) :: {:error, any()} | {:ok, any()} - def fetch_sms(attrs) do - username = Application.get_env(:at_ex, :username) - - params = - attrs - |> Map.put(:username, username) - |> Map.to_list() - - with {:ok, %{status: 200} = res} <- get("/messaging", query: params) do - {:ok, Jason.decode!(res.body)} - else - {:ok, val} -> - {:error, %{status: val.status, message: val.body}} - - {:error, message} -> - {:error, message} - end - end - # Checkout token endpoints @live_token_url "https://api.africastalking.com/checkout/token" @sandbox_token_url "https://api.sandbox.africastalking.com/checkout/token" - # Using this system for delivery of which URL to use (sandbox or live) + # Using this system for delivery of which URL to use (sandbox or live) # determined by whether we ar in production or development or test # Selection of the live URL can be forced by setting an environment # variable FORCE_TOKEN_LIVE=YES @@ -82,7 +20,7 @@ defmodule AtEx.Gateway.Sms do This function fetches the checkout token from the checkout token endpoint THIS IS DIFFERENT THAN THE SMS ENDPOINT!! - phone_number: - a string representing a valid phone number in EL64 + phone_number: - a string representing a valid phone number in EL64 (+ with all non digit characters removed) [NOTE: function does not verify the phone number is in any way correct before sending to the endpoint.] @@ -136,22 +74,22 @@ defmodule AtEx.Gateway.Sms do - `phoneNumber` - phone number to be subscribed ## Example - iex> AtEx.Gateway.Sms.create_subscription(%{ - ...> shortCode: "1234", - ...> keyword: "keyword", - ...> phoneNumber: "+2541231111" - ...> }) + iex> AtEx.Gateway.Sms.PremiumSubscriptions.create_subscription(%{phoneNumber: "+2541231111"}) {:ok, result} """ @spec create_subscription(map()) :: {:error, any()} | {:ok, any()} def create_subscription(%{phoneNumber: phone_number} = attrs) do username = Application.get_env(:at_ex, :username) + shortcode = Application.get_env(:at_ex, :short_code) + keyword = Application.get_env(:at_ex, :keyword) case generate_checkout_token(phone_number) do {:ok, token} -> params = attrs |> Map.put(:username, username) + |> Map.put(:shortCode, shortcode) + |> Map.put(:keyword, keyword) |> Map.put(:checkoutToken, token) with {:ok, %{status: 201} = res} <- post("/subscription/create", params) do @@ -181,19 +119,23 @@ defmodule AtEx.Gateway.Sms do - `lastReceivedId` - (optional) ID of the subscription you believe to be your last. Set it to 0 to for the first time. ## Example - iex> AtEx.Gateway.Sms.create_subscription(%{ - ...> shortCode: "1234", - ...> keyword: "keyword", - ...> }) - {:ok, result} + iex> AtEx.Gateway.Sms.create_subscription(%{ + ...> shortCode: "1234", + ...> keyword: "keyword", + ...> }) + {:ok, result} """ - @spec fetch_subscriptions(map()) :: {:error, any()} | {:ok, any()} - def fetch_subscriptions(attrs) do + @spec fetch_subscriptions() :: {:error, any()} | {:ok, any()} + def fetch_subscriptions() do username = Application.get_env(:at_ex, :username) + shortcode = Application.get_env(:at_ex, :short_code) + keyword = Application.get_env(:at_ex, :keyword) params = - attrs + %{} |> Map.put(:username, username) + |> Map.put(:shortCode, shortcode) + |> Map.put(:keyword, keyword) with {:ok, %{status: 200} = res} <- get("/subscription", query: params) do {:ok, Jason.decode!(res.body)} @@ -217,20 +159,20 @@ defmodule AtEx.Gateway.Sms do - `phoneNumber` - phone number to be unsubscribed ## Example - iex> AtEx.Gateway.Sms.delete_subscription(%{ - ...> shortCode: "1234", - ...> keyword: "keyword", - ...> phoneNumber: "+2541231111" - ...> }) + iex> AtEx.Gateway.Sms.delete_subscription(%{ phoneNumber: "+2541231111"}) {:ok, %{"description" => "Succeeded", "status" => "Success"}} """ @spec delete_subscription(map()) :: {:error, any()} | {:ok, any()} def delete_subscription(attrs) do username = Application.get_env(:at_ex, :username) + shortcode = Application.get_env(:at_ex, :short_code) + keyword = Application.get_env(:at_ex, :keyword) params = attrs |> Map.put(:username, username) + |> Map.put(:shortCode, shortcode) + |> Map.put(:keyword, keyword) with {:ok, %{status: 201} = res} <- post("/subscription/delete", params) do {:ok, Jason.decode!(res.body)} diff --git a/lib/at_ex/gateway/base_http.ex b/lib/at_ex/gateway/base_http.ex index 1c2e6ee..6b5f73a 100644 --- a/lib/at_ex/gateway/base_http.ex +++ b/lib/at_ex/gateway/base_http.ex @@ -48,13 +48,21 @@ defmodule AtEx.Gateway.Base do @doc """ Process results from calling the gateway """ - def process_result(result) do - with {:ok, res} <- Jason.decode(result) do - {:ok, res} - else - {:error, val} -> - {:error, val} - end + + def process_result({:ok, %{status: 200} = res}) do + Jason.decode(res.body) + end + + def process_result({:ok, %{status: 201} = res}) do + Jason.decode(res.body) + end + + def process_result({:ok, result}) do + {:error, %{status: result.status, message: result.body}} + end + + def process_result({:error, result}) do + {:error, result} end end end diff --git a/lib/at_ex/gateway/voice.ex b/lib/at_ex/gateway/voice.ex new file mode 100644 index 0000000..e1c292c --- /dev/null +++ b/lib/at_ex/gateway/voice.ex @@ -0,0 +1,2 @@ +defmodule AtEx.Gateway.Voice do +end diff --git a/lib/at_ex/sms/sms.ex b/lib/at_ex/sms/sms.ex deleted file mode 100644 index 8b13789..0000000 --- a/lib/at_ex/sms/sms.ex +++ /dev/null @@ -1 +0,0 @@ - diff --git a/test/at_ex/gateway/Sms/bulk_test.exs b/test/at_ex/gateway/Sms/bulk_test.exs new file mode 100644 index 0000000..fc213de --- /dev/null +++ b/test/at_ex/gateway/Sms/bulk_test.exs @@ -0,0 +1,99 @@ +defmodule AtEx.Gateway.Sms.BulkTest do + @moduledoc """ + This module holds unit tests for the functions in the SMS gateway + """ + use ExUnit.Case + + alias AtEx.Gateway.Sms.Bulk + + @attr "username=" + + setup do + Tesla.Mock.mock(fn + %{method: :post, body: @attr} -> + %Tesla.Env{ + status: 400, + body: "Request is missing required form field 'to'" + } + + %{method: :post} -> + %Tesla.Env{ + status: 201, + body: + Jason.encode!(%{ + "SMSMessageData" => %{ + "Message" => "Sent to 1/1 Total Cost: ZAR 0.1124", + "Recipients" => [ + %{ + "cost" => "KES 0.8000", + "messageId" => "ATXid_a584c3fd712a00b7bce3c4b7b552ac56", + "number" => "+254728833181", + "status" => "Success", + "statusCode" => 101 + } + ] + } + }) + } + + %{method: :get} -> + %Tesla.Env{ + status: 200, + body: + Jason.encode!(%{ + "SMSMessageData" => %{ + "Messages" => [ + %{ + linkId: "SampleLinkId123", + text: "Hello", + to: "28901", + id: 15071, + date: "2018-03-19T08:34:18.445Z", + from: "+254711XXXYYY" + } + ] + } + }) + } + end) + + :ok + end + + describe "Sms Gateway" do + test "sends_sms/1 should send sms with required parameters" do + # make message details + send_details = %{to: "+254728833181", message: "new music"} + + # run details through our code + {:ok, result} = Bulk.send_sms(send_details) + + # assert our code gives us a single element list of messages + [msg] = result["SMSMessageData"]["Recipients"] + + # assert that message details correspond to details of set up message + assert msg["number"] == send_details.to + end + + test "sends_sms/1 should error out without phone number parameter" do + # run details through our code + {:error, result} = Bulk.send_sms(%{}) + + "Request is missing required form field 'to'" = result.message + + 400 = result.status + end + + test "fetches sms collects data with correct params" do + send_details = %{username: "sandbox"} + + # run details through our code + {:ok, result} = Bulk.fetch_sms(send_details) + # assert our code gives us a single element list of messages + [msg] = result["SMSMessageData"]["Messages"] + + # assert that message details correspond to details of set up message + assert msg["text"] == "Hello" + end + end +end diff --git a/test/at_ex/gateway/sms_test.exs b/test/at_ex/gateway/Sms/premium_test.exs similarity index 78% rename from test/at_ex/gateway/sms_test.exs rename to test/at_ex/gateway/Sms/premium_test.exs index 003f95f..17edf21 100644 --- a/test/at_ex/gateway/sms_test.exs +++ b/test/at_ex/gateway/Sms/premium_test.exs @@ -4,11 +4,11 @@ defmodule AtEx.Gateway.SmsTest do """ use ExUnit.Case - alias AtEx.Gateway.Sms + alias AtEx.Gateway.Sms.PremiumSubscriptions @attr "username=" - # Endpoint for getting the checkout token + # Endpoint for getting the checkout token # Unless overridden, we will always use the sandbox URL during test # If overridden, all the checkout token tests will fail. @checkout_token_url "https://api.sandbox.africastalking.com/checkout/token/create" @@ -72,46 +72,10 @@ defmodule AtEx.Gateway.SmsTest do :ok end - describe "Sms Gateway" do - test "sends_sms/1 should send sms with required parameters" do - # make message details - send_details = %{to: "+254728833181", message: "new music"} - - # run details through our code - {:ok, result} = Sms.send_sms(send_details) - - # assert our code gives us a single element list of messages - [msg] = result["SMSMessageData"]["Recipients"] - - # assert that message details correspond to details of set up message - assert msg["number"] == send_details.to - end - - test "sends_sms/1 should error out without phone number parameter" do - # run details through our code - {:error, result} = Sms.send_sms(%{}) - - "Request is missing required form field 'to'" = result.message - - 400 = result.status - end - - test "fetches sms collects data with correct params" do - send_details = %{username: "sandbox"} - - # run details through our code - {:ok, result} = Sms.fetch_sms(send_details) - # assert our code gives us a single element list of messages - [msg] = result["SMSMessageData"]["Messages"] - - # assert that message details correspond to details of set up message - assert msg["text"] == "Hello" - end - - # Checkout token tests need their own mock calls, or we would need + describe "Sms Gateway/Premium" do + # Checkout token tests need their own mock calls, or we would need # separate phone numbers for each test. This way values can be # reused. - test "fetch checkout token successfully" do Tesla.Mock.mock(fn %{method: :post, url: @checkout_token_url, body: @checkout_token_query} -> @@ -125,7 +89,9 @@ defmodule AtEx.Gateway.SmsTest do } end) - assert {:ok, token} = Sms.generate_checkout_token(@checkout_token_phonenumber) + assert {:ok, token} = + PremiumSubscriptions.generate_checkout_token(@checkout_token_phonenumber) + assert token == @checkout_token end @@ -142,7 +108,8 @@ defmodule AtEx.Gateway.SmsTest do } end) - assert {:error, message} = Sms.generate_checkout_token(@checkout_token_phonenumber) + assert {:error, message} = + PremiumSubscriptions.generate_checkout_token(@checkout_token_phonenumber) assert message == "Failure - Error Message" end @@ -156,7 +123,9 @@ defmodule AtEx.Gateway.SmsTest do } end) - assert {:error, message} = Sms.generate_checkout_token(@checkout_token_phonenumber) + assert {:error, message} = + PremiumSubscriptions.generate_checkout_token(@checkout_token_phonenumber) + assert message = "500 - Error Message" end @@ -173,7 +142,8 @@ defmodule AtEx.Gateway.SmsTest do } end) - assert {:error, message} = Sms.generate_checkout_token(@checkout_token_phonenumber) + assert {:error, message} = + PremiumSubscriptions.generate_checkout_token(@checkout_token_phonenumber) assert message == "Failure - Potential Error Message" end @@ -202,7 +172,7 @@ defmodule AtEx.Gateway.SmsTest do end) assert {:ok, response} = - Sms.create_subscription(%{ + PremiumSubscriptions.create_subscription(%{ phoneNumber: @checkout_token_phonenumber, shortCode: "12345", keyword: "music" @@ -235,7 +205,7 @@ defmodule AtEx.Gateway.SmsTest do end) assert {:ok, response} = - Sms.create_subscription(%{ + PremiumSubscriptions.create_subscription(%{ phoneNumber: @checkout_token_phonenumber, shortCode: "99999", keyword: "music" @@ -255,18 +225,16 @@ defmodule AtEx.Gateway.SmsTest do %{ "date" => "2019-10-05T18:57:08.000", "id" => 2_007_800, - "phoneNumber" => "+254711123123" + "phoneNumber" => "+254711123123", + "shortCode" => "12345", + "keyword" => "music" } ] }) } end) - assert {:ok, response} = - Sms.fetch_subscriptions(%{ - shortCode: "12345", - keyword: "music" - }) + assert {:ok, response} = PremiumSubscriptions.fetch_subscriptions() assert Enum.count(response["responses"]) == 1 end @@ -280,10 +248,7 @@ defmodule AtEx.Gateway.SmsTest do } end) - assert {:error, response} = - Sms.fetch_subscriptions(%{ - keyword: "music" - }) + assert {:error, response} = PremiumSubscriptions.fetch_subscriptions() assert response.message == "Request is missing required query parameter 'shortCode'" end @@ -302,7 +267,7 @@ defmodule AtEx.Gateway.SmsTest do end) assert {:ok, response} = - Sms.delete_subscription(%{ + PremiumSubscriptions.delete_subscription(%{ phoneNumber: @checkout_token_phonenumber, shortCode: "12345", keyword: "music" @@ -326,7 +291,7 @@ defmodule AtEx.Gateway.SmsTest do end) assert {:ok, response} = - Sms.delete_subscription(%{ + PremiumSubscriptions.delete_subscription(%{ phoneNumber: @checkout_token_phonenumber, shortCode: "99900", keyword: "music"