From 3aa7471f55c5f35b6304a3038a1a89f73e7dd556 Mon Sep 17 00:00:00 2001 From: Patrick Date: Tue, 23 Sep 2025 10:15:02 +0300 Subject: [PATCH] improve documentation in the main module --- lib/momoapi_elixir.ex | 263 ++++++++++++++++++++-- lib/momoapi_elixir/collections.ex | 29 +-- lib/momoapi_elixir/disbursements.ex | 29 +-- lib/momoapi_elixir/mix/tasks/provision.ex | 4 +- mix.lock | 2 +- test/collections_test.exs | 5 +- test/disbursements_test.exs | 5 +- 7 files changed, 277 insertions(+), 60 deletions(-) diff --git a/lib/momoapi_elixir.ex b/lib/momoapi_elixir.ex index 4f17ccf..011bde7 100644 --- a/lib/momoapi_elixir.ex +++ b/lib/momoapi_elixir.ex @@ -7,6 +7,8 @@ defmodule MomoapiElixir do ## Quick Start + Set up configuration and handle responses properly: + # Option 1: Use environment variables (Recommended for production) {:ok, config} = MomoapiElixir.Config.from_env() @@ -31,25 +33,35 @@ defmodule MomoapiElixir do payeeNote: "Thank you" } - {:ok, reference_id} = MomoapiElixir.Collections.request_to_pay(config, payment) + # Proper error handling with case pattern matching + case MomoapiElixir.Collections.request_to_pay(config, payment) do + {:ok, reference_id} -> + # Payment initiated successfully + reference_id + {:error, reason} -> + # Handle error appropriately + reason + end - # Disbursements - Transfer money to payee - transfer = %{ - amount: "50", - currency: "UGX", - externalId: "transfer_456", - payee: %{ - partyIdType: "MSISDN", - partyId: "256784123456" - } - } + ## Main Functions + + This module provides convenient wrapper functions for the most common operations: + + ### Collections (Payments from consumers) + - `request_to_pay/2` - Request payment from a consumer + - `get_payment_status/2` - Check payment transaction status + - `get_collections_balance/1` - Get Collections account balance - {:ok, reference_id} = MomoapiElixir.Disbursements.transfer(config, transfer) + ### Disbursements (Transfers to payees) + - `transfer/2` - Transfer money to a payee + - `get_transfer_status/2` - Check transfer transaction status + - `get_disbursements_balance/1` - Get Disbursements account balance - ## Modules + ## Core Modules - `MomoapiElixir.Collections` - Collections API functions - `MomoapiElixir.Disbursements` - Disbursements API functions + - `MomoapiElixir.Config` - Configuration management - `MomoapiElixir.Auth` - Authentication utilities - `MomoapiElixir.Validator` - Request validation utilities """ @@ -67,6 +79,53 @@ defmodule MomoapiElixir do @doc """ Request a payment from a consumer (Collections API). + The payer will be asked to authorize the payment. The transaction will be + executed once the payer has authorized the payment. Returns a reference ID + that can be used to check the transaction status. + + ## Parameters + + - `config` - Configuration map with subscription_key, user_id, api_key, and target_environment + - `body` - Payment request map with the following required fields: + - `amount` - Payment amount as string (e.g., "100") + - `currency` - ISO 4217 currency code (e.g., "UGX") + - `externalId` - Your unique transaction identifier + - `payer` - Map with `partyIdType` ("MSISDN" or "EMAIL") and `partyId` + - `payerMessage` - Message shown to the payer (optional) + - `payeeNote` - Internal note for the payee (optional) + + ## Examples + + config = %{ + subscription_key: "your_key", + user_id: "your_user_id", + api_key: "your_api_key", + target_environment: "sandbox" + } + + payment = %{ + amount: "1000", + currency: "UGX", + externalId: "payment_123", + payer: %{ + partyIdType: "MSISDN", + partyId: "256784123456" + }, + payerMessage: "Payment for goods", + payeeNote: "Thank you" + } + + case MomoapiElixir.request_to_pay(config, payment) do + {:ok, reference_id} -> + IO.puts("Payment initiated: \#{reference_id}") + {:error, validation_errors} when is_list(validation_errors) -> + IO.puts("Validation failed: \#{inspect(validation_errors)}") + {:error, %{status_code: status, body: body}} -> + IO.puts("API error \#{status}: \#{inspect(body)}") + {:error, reason} -> + IO.puts("Request failed: \#{inspect(reason)}") + end + This is a convenience function that delegates to `MomoapiElixir.Collections.request_to_pay/2`. """ @spec request_to_pay(config(), map()) :: {:ok, String.t()} | {:error, term()} @@ -77,6 +136,52 @@ defmodule MomoapiElixir do @doc """ Transfer money to a payee (Disbursements API). + Used to transfer an amount from the owner's account to a payee account. + Returns a reference ID which can be used to check the transaction status. + + ## Parameters + + - `config` - Configuration map with subscription_key, user_id, api_key, and target_environment + - `body` - Transfer request map with the following required fields: + - `amount` - Transfer amount as string (e.g., "50") + - `currency` - ISO 4217 currency code (e.g., "UGX") + - `externalId` - Your unique transaction identifier + - `payee` - Map with `partyIdType` ("MSISDN" or "EMAIL") and `partyId` + - `payerMessage` - Message for the transfer (optional) + - `payeeNote` - Note for the payee (optional) + + ## Examples + + config = %{ + subscription_key: "your_key", + user_id: "your_user_id", + api_key: "your_api_key", + target_environment: "sandbox" + } + + transfer = %{ + amount: "2500", + currency: "UGX", + externalId: "transfer_456", + payee: %{ + partyIdType: "MSISDN", + partyId: "256784987654" + }, + payerMessage: "Salary payment", + payeeNote: "Monthly salary" + } + + case MomoapiElixir.transfer(config, transfer) do + {:ok, reference_id} -> + IO.puts("Transfer initiated: \#{reference_id}") + {:error, validation_errors} when is_list(validation_errors) -> + IO.puts("Validation failed: \#{inspect(validation_errors)}") + {:error, %{status_code: status, body: body}} -> + IO.puts("API error \#{status}: \#{inspect(body)}") + {:error, reason} -> + IO.puts("Transfer failed: \#{inspect(reason)}") + end + This is a convenience function that delegates to `MomoapiElixir.Disbursements.transfer/2`. """ @spec transfer(config(), map()) :: {:ok, String.t()} | {:error, term()} @@ -87,6 +192,31 @@ defmodule MomoapiElixir do @doc """ Get Collections account balance. + Retrieves the current available balance for the Collections account. + Useful for checking available funds before requesting payments. + + ## Parameters + + - `config` - Configuration map with subscription_key, user_id, api_key, and target_environment + + ## Examples + + config = %{ + subscription_key: "your_key", + user_id: "your_user_id", + api_key: "your_api_key", + target_environment: "sandbox" + } + + case MomoapiElixir.get_collections_balance(config) do + {:ok, %{"availableBalance" => balance, "currency" => currency}} -> + IO.puts("Collections balance: \#{balance} \#{currency}") + {:error, %{status_code: status, body: body}} -> + IO.puts("Failed to get balance: \#{status} - \#{inspect(body)}") + {:error, reason} -> + IO.puts("Request failed: \#{inspect(reason)}") + end + This is a convenience function that delegates to `MomoapiElixir.Collections.get_balance/1`. """ @spec get_collections_balance(config()) :: {:ok, map()} | {:error, term()} @@ -97,10 +227,117 @@ defmodule MomoapiElixir do @doc """ Get Disbursements account balance. + Retrieves the current available balance for the Disbursements account. + Useful for checking available funds before initiating transfers. + + ## Parameters + + - `config` - Configuration map with subscription_key, user_id, api_key, and target_environment + + ## Examples + + config = %{ + subscription_key: "your_key", + user_id: "your_user_id", + api_key: "your_api_key", + target_environment: "sandbox" + } + + case MomoapiElixir.get_disbursements_balance(config) do + {:ok, %{"availableBalance" => balance, "currency" => currency}} -> + IO.puts("Disbursements balance: \#{balance} \#{currency}") + {:error, %{status_code: 401}} -> + IO.puts("Authentication failed - check your credentials") + {:error, %{status_code: status, body: body}} -> + IO.puts("Failed to get balance: \#{status} - \#{inspect(body)}") + {:error, reason} -> + IO.puts("Request failed: \#{inspect(reason)}") + end + This is a convenience function that delegates to `MomoapiElixir.Disbursements.get_balance/1`. """ @spec get_disbursements_balance(config()) :: {:ok, map()} | {:error, term()} def get_disbursements_balance(config) do Disbursements.get_balance(config) end + + @doc """ + Get Collections transaction status. + + Retrieve transaction information using the reference ID returned from `request_to_pay/2`. + You can invoke this at intervals until the transaction fails or succeeds. + + ## Parameters + + - `config` - Configuration map with subscription_key, user_id, api_key, and target_environment + - `reference_id` - The reference ID returned from the payment request + + ## Examples + + config = %{ + subscription_key: "your_key", + user_id: "your_user_id", + api_key: "your_api_key", + target_environment: "sandbox" + } + + case MomoapiElixir.get_payment_status(config, reference_id) do + {:ok, %{"status" => "SUCCESSFUL", "amount" => amount}} -> + IO.puts("Payment of \#{amount} completed successfully!") + {:ok, %{"status" => "PENDING"}} -> + IO.puts("Payment is still processing...") + {:ok, %{"status" => "FAILED", "reason" => reason}} -> + IO.puts("Payment failed: \#{reason}") + {:error, %{status_code: status, body: body}} -> + IO.puts("Failed to get status: \#{status} - \#{inspect(body)}") + {:error, reason} -> + IO.puts("Request failed: \#{inspect(reason)}") + end + + This is a convenience function that delegates to `MomoapiElixir.Collections.get_transaction_status/2`. + """ + @spec get_payment_status(config(), String.t()) :: {:ok, map()} | {:error, term()} + def get_payment_status(config, reference_id) do + Collections.get_transaction_status(config, reference_id) + end + + @doc """ + Get Disbursements transaction status. + + Retrieve transaction information using the reference ID returned from `transfer/2`. + You can invoke this at intervals until the transaction fails or succeeds. + + ## Parameters + + - `config` - Configuration map with subscription_key, user_id, api_key, and target_environment + - `reference_id` - The reference ID returned from the transfer request + + ## Examples + + config = %{ + subscription_key: "your_key", + user_id: "your_user_id", + api_key: "your_api_key", + target_environment: "sandbox" + } + + case MomoapiElixir.get_transfer_status(config, reference_id) do + {:ok, %{"status" => "SUCCESSFUL", "amount" => amount, "externalId" => external_id}} -> + IO.puts("Transfer \#{external_id} of \#{amount} completed successfully!") + {:ok, %{"status" => "PENDING"}} -> + IO.puts("Transfer is still processing...") + {:ok, %{"status" => "FAILED", "reason" => reason}} -> + IO.puts("Transfer failed: \#{reason}") + {:error, %{status_code: status, body: body}} -> + IO.puts("Failed to get status: \#{status} - \#{inspect(body)}") + {:error, reason} -> + IO.puts("Request failed: \#{inspect(reason)}") + end + + This is a convenience function that delegates to `MomoapiElixir.Disbursements.get_transaction_status/2`. + """ + @spec get_transfer_status(config(), String.t()) :: {:ok, map()} | {:error, term()} + def get_transfer_status(config, reference_id) do + Disbursements.get_transaction_status(config, reference_id) + end end diff --git a/lib/momoapi_elixir/collections.ex b/lib/momoapi_elixir/collections.ex index 0ae3f2d..598200a 100644 --- a/lib/momoapi_elixir/collections.ex +++ b/lib/momoapi_elixir/collections.ex @@ -98,10 +98,11 @@ defmodule MomoapiElixir.Collections do end defp build_headers(token, config, reference_id \\ nil) do + target_env = Map.get(config, :target_environment, "sandbox") base_headers = [ {"Authorization", "Bearer #{token}"}, - {"Ocp-Apim-Subscription-Key", config.subscription_key}, - {"X-Target-Environment", config.target_environment || "sandbox"} + {"Ocp-Apim-Subscription-Key", Map.get(config, :subscription_key)}, + {"X-Target-Environment", target_env} ] case reference_id do @@ -110,42 +111,30 @@ defmodule MomoapiElixir.Collections do end end - defp handle_payment_response({:ok, %{status_code: 202}}, reference_id) do + defp handle_payment_response(%{status_code: 202}, reference_id) do {:ok, reference_id} end - defp handle_payment_response({:ok, %{status_code: status_code, body: body}}, _reference_id) do + defp handle_payment_response(%{status_code: status_code, body: body}, _reference_id) do {:error, %{status_code: status_code, body: decode_body(body)}} end - defp handle_payment_response({:error, reason}, _reference_id) do - {:error, reason} - end - - defp handle_balance_response({:ok, %{status_code: 200, body: body}}) do + defp handle_balance_response(%{status_code: 200, body: body}) do {:ok, decode_body(body)} end - defp handle_balance_response({:ok, %{status_code: status_code, body: body}}) do + defp handle_balance_response(%{status_code: status_code, body: body}) do {:error, %{status_code: status_code, body: decode_body(body)}} end - defp handle_balance_response({:error, reason}) do - {:error, reason} - end - - defp handle_transaction_response({:ok, %{status_code: 200, body: body}}) do + defp handle_transaction_response(%{status_code: 200, body: body}) do {:ok, decode_body(body)} end - defp handle_transaction_response({:ok, %{status_code: status_code, body: body}}) do + defp handle_transaction_response(%{status_code: status_code, body: body}) do {:error, %{status_code: status_code, body: decode_body(body)}} end - defp handle_transaction_response({:error, reason}) do - {:error, reason} - end - defp decode_body(""), do: %{} defp decode_body(body) when is_binary(body) do case Poison.decode(body) do diff --git a/lib/momoapi_elixir/disbursements.ex b/lib/momoapi_elixir/disbursements.ex index 51efaff..4b8050f 100644 --- a/lib/momoapi_elixir/disbursements.ex +++ b/lib/momoapi_elixir/disbursements.ex @@ -96,10 +96,11 @@ defmodule MomoapiElixir.Disbursements do end defp build_headers(token, config, reference_id \\ nil) do + target_env = Map.get(config, :target_environment, "sandbox") base_headers = [ {"Authorization", "Bearer #{token}"}, - {"Ocp-Apim-Subscription-Key", config.subscription_key}, - {"X-Target-Environment", config.target_environment || "sandbox"} + {"Ocp-Apim-Subscription-Key", Map.get(config, :subscription_key)}, + {"X-Target-Environment", target_env} ] case reference_id do @@ -108,42 +109,30 @@ defmodule MomoapiElixir.Disbursements do end end - defp handle_transfer_response({:ok, %{status_code: 202}}, reference_id) do + defp handle_transfer_response(%{status_code: 202}, reference_id) do {:ok, reference_id} end - defp handle_transfer_response({:ok, %{status_code: status_code, body: body}}, _reference_id) do + defp handle_transfer_response(%{status_code: status_code, body: body}, _reference_id) do {:error, %{status_code: status_code, body: decode_body(body)}} end - defp handle_transfer_response({:error, reason}, _reference_id) do - {:error, reason} - end - - defp handle_balance_response({:ok, %{status_code: 200, body: body}}) do + defp handle_balance_response(%{status_code: 200, body: body}) do {:ok, decode_body(body)} end - defp handle_balance_response({:ok, %{status_code: status_code, body: body}}) do + defp handle_balance_response(%{status_code: status_code, body: body}) do {:error, %{status_code: status_code, body: decode_body(body)}} end - defp handle_balance_response({:error, reason}) do - {:error, reason} - end - - defp handle_transaction_response({:ok, %{status_code: 200, body: body}}) do + defp handle_transaction_response(%{status_code: 200, body: body}) do {:ok, decode_body(body)} end - defp handle_transaction_response({:ok, %{status_code: status_code, body: body}}) do + defp handle_transaction_response(%{status_code: status_code, body: body}) do {:error, %{status_code: status_code, body: decode_body(body)}} end - defp handle_transaction_response({:error, reason}) do - {:error, reason} - end - defp decode_body(""), do: %{} defp decode_body(body) when is_binary(body) do case Poison.decode(body) do diff --git a/lib/momoapi_elixir/mix/tasks/provision.ex b/lib/momoapi_elixir/mix/tasks/provision.ex index 2758232..63f816a 100644 --- a/lib/momoapi_elixir/mix/tasks/provision.ex +++ b/lib/momoapi_elixir/mix/tasks/provision.ex @@ -39,11 +39,11 @@ defmodule Mix.Tasks.Provision do end def run([_subscription_key]) do - raise "One of the arguments has not been provided. Please provide subscription key for arg 1 and callback for arg 2" + Mix.shell().error("One of the arguments has not been provided. Please provide subscription key for arg 1 and callback for arg 2") end def run([]) do - raise "Both subscription_key and callback host are required" + Mix.shell().error("Both subscription_key and callback host are required") end defp reference_id do diff --git a/mix.lock b/mix.lock index d5f2d39..245053c 100644 --- a/mix.lock +++ b/mix.lock @@ -16,7 +16,7 @@ "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, "uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm", "c790593b4c3b601f5dc2378baae7efaf5b3d73c4c6456ba85759905be792f2ac"}, } diff --git a/test/collections_test.exs b/test/collections_test.exs index a87f3a5..e84f088 100644 --- a/test/collections_test.exs +++ b/test/collections_test.exs @@ -163,9 +163,10 @@ defmodule MomoapiElixir.CollectionsTest do # Mock transaction status request ClientMock - |> expect(:get, fn "/collection/v1_0/requesttopay/" <> ^reference_id, headers -> + |> expect(:get, fn "/collection/v1_0/requesttopay/" <> id, headers -> + assert id == reference_id assert {"Authorization", "Bearer test_token"} in headers - assert {"X-Reference-Id", ^reference_id} in headers + assert {"X-Reference-Id", reference_id} in headers {:ok, %{status_code: 200, body: Poison.encode!(expected_transaction)}} end) diff --git a/test/disbursements_test.exs b/test/disbursements_test.exs index ab26203..59365a0 100644 --- a/test/disbursements_test.exs +++ b/test/disbursements_test.exs @@ -165,9 +165,10 @@ defmodule MomoapiElixir.DisbursementsTest do # Mock transaction status request ClientMock - |> expect(:get, fn "/disbursement/v1_0/transfer/" <> ^reference_id, headers -> + |> expect(:get, fn "/disbursement/v1_0/transfer/" <> id, headers -> + assert id == reference_id assert {"Authorization", "Bearer test_token"} in headers - assert {"X-Reference-Id", ^reference_id} in headers + assert {"X-Reference-Id", reference_id} in headers {:ok, %{status_code: 200, body: Poison.encode!(expected_transaction)}} end)