From 8944d2bb756b5c0224e9eebd59a7575532250dbe Mon Sep 17 00:00:00 2001 From: Patrick Date: Tue, 23 Sep 2025 11:35:11 +0300 Subject: [PATCH] add request_to_withdraw, get_basic_user_info and validate_account_holder_status --- lib/momoapi_elixir.ex | 166 ++++++++++++++++++++++++++++++ lib/momoapi_elixir/collections.ex | 125 +++++++++++++++++++++- 2 files changed, 289 insertions(+), 2 deletions(-) diff --git a/lib/momoapi_elixir.ex b/lib/momoapi_elixir.ex index 011bde7..3fcdbd5 100644 --- a/lib/momoapi_elixir.ex +++ b/lib/momoapi_elixir.ex @@ -49,8 +49,11 @@ defmodule MomoapiElixir do ### Collections (Payments from consumers) - `request_to_pay/2` - Request payment from a consumer + - `request_to_withdraw/2` - Request withdrawal from a consumer account - `get_payment_status/2` - Check payment transaction status - `get_collections_balance/1` - Get Collections account balance + - `get_basic_user_info/2` - Get basic user information for an account holder (defaults to MSISDN) + - `validate_account_holder_status/2` - Validate if account holder is active (defaults to MSISDN) ### Disbursements (Transfers to payees) - `transfer/2` - Transfer money to a payee @@ -340,4 +343,167 @@ defmodule MomoapiElixir do def get_transfer_status(config, reference_id) do Disbursements.get_transaction_status(config, reference_id) end + + @doc """ + Request to withdraw money from a consumer account (Collections API). + + This function allows you to request a withdrawal from a consumer's account. + The consumer will be asked to authorize the withdrawal. 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` - Withdrawal request map with the following required fields: + - `amount` - Withdrawal 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" + } + + withdrawal = %{ + amount: "500", + currency: "UGX", + externalId: "withdraw_789", + payer: %{ + partyIdType: "MSISDN", + partyId: "256784123456" + }, + payerMessage: "Cash withdrawal", + payeeNote: "ATM withdrawal" + } + + case MomoapiElixir.request_to_withdraw(config, withdrawal) do + {:ok, reference_id} -> + IO.puts("Withdrawal 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("Withdrawal failed: \#{inspect(reason)}") + end + + This is a convenience function that delegates to `MomoapiElixir.Collections.request_to_withdraw/2`. + """ + @spec request_to_withdraw(config(), map()) :: {:ok, String.t()} | {:error, term()} + def request_to_withdraw(config, body) do + Collections.request_to_withdraw(config, body) + end + + @doc """ + Get basic user information for an account holder. + + Retrieve basic user information such as names for a specific account holder + using their party ID type and party ID. + + ## Parameters + + - `config` - Configuration map with subscription_key, user_id, api_key, and target_environment + - `account_holder_id_type` - Type of account identifier (defaults to "MSISDN") + - "MSISDN" - Mobile phone number + - "EMAIL" - Email address + - `account_holder_id` - The account identifier (phone number or email) + + ## Examples + + config = %{ + subscription_key: "your_key", + user_id: "your_user_id", + api_key: "your_api_key", + target_environment: "sandbox" + } + + # Using default MSISDN type (most common) + case MomoapiElixir.get_basic_user_info(config, "256784123456") do + {:ok, %{"given_name" => first_name, "family_name" => last_name}} -> + IO.puts("User: \#{first_name} \#{last_name}") + {:ok, user_info} -> + IO.puts("User info: \#{inspect(user_info)}") + {:error, %{status_code: 404}} -> + IO.puts("User not found") + {:error, %{status_code: status, body: body}} -> + IO.puts("Failed to get user info: \#{status} - \#{inspect(body)}") + {:error, reason} -> + IO.puts("Request failed: \#{inspect(reason)}") + end + + # Explicitly specifying EMAIL type + case MomoapiElixir.get_basic_user_info(config, "EMAIL", "user@example.com") do + {:ok, user_info} -> + IO.puts("Email user info: \#{inspect(user_info)}") + {:error, reason} -> + IO.puts("Failed: \#{inspect(reason)}") + end + + This is a convenience function that delegates to `MomoapiElixir.Collections.get_basic_user_info/2` or `/3`. + """ + @spec get_basic_user_info(config(), String.t(), String.t()) :: {:ok, map()} | {:error, term()} + @spec get_basic_user_info(config(), String.t()) :: {:ok, map()} | {:error, term()} + def get_basic_user_info(config, account_holder_id_type \\ "MSISDN", account_holder_id) do + Collections.get_basic_user_info(config, account_holder_id_type, account_holder_id) + end + + @doc """ + Validate account holder status. + + Check if an account holder is active and able to receive transactions. + This is useful for validating account details before initiating transactions. + + ## Parameters + + - `config` - Configuration map with subscription_key, user_id, api_key, and target_environment + - `account_holder_id_type` - Type of account identifier (defaults to "MSISDN") + - "MSISDN" - Mobile phone number + - "EMAIL" - Email address + - `account_holder_id` - The account identifier (phone number or email) + + ## Examples + + config = %{ + subscription_key: "your_key", + user_id: "your_user_id", + api_key: "your_api_key", + target_environment: "sandbox" + } + + # Using default MSISDN type (most common) + case MomoapiElixir.validate_account_holder_status(config, "256784123456") do + {:ok, %{"result" => true}} -> + IO.puts("Account is active and valid") + {:ok, %{"result" => false}} -> + IO.puts("Account is inactive or invalid") + {:error, %{status_code: 404}} -> + IO.puts("Account not found") + {:error, %{status_code: status, body: body}} -> + IO.puts("Failed to validate account: \#{status} - \#{inspect(body)}") + {:error, reason} -> + IO.puts("Request failed: \#{inspect(reason)}") + end + + # Explicitly specifying EMAIL type + case MomoapiElixir.validate_account_holder_status(config, "EMAIL", "user@example.com") do + {:ok, %{"result" => status}} -> + IO.puts("Email account status: \#{status}") + {:error, reason} -> + IO.puts("Failed: \#{inspect(reason)}") + end + + This is a convenience function that delegates to `MomoapiElixir.Collections.validate_account_holder_status/2` or `/3`. + """ + @spec validate_account_holder_status(config(), String.t(), String.t()) :: {:ok, map()} | {:error, term()} + @spec validate_account_holder_status(config(), String.t()) :: {:ok, map()} | {:error, term()} + def validate_account_holder_status(config, account_holder_id_type \\ "MSISDN", account_holder_id) do + Collections.validate_account_holder_status(config, account_holder_id_type, account_holder_id) + end end diff --git a/lib/momoapi_elixir/collections.ex b/lib/momoapi_elixir/collections.ex index 598200a..e7898fd 100644 --- a/lib/momoapi_elixir/collections.ex +++ b/lib/momoapi_elixir/collections.ex @@ -2,8 +2,12 @@ defmodule MomoapiElixir.Collections do @moduledoc """ Collections API for MTN Mobile Money. - This module provides functions to request payments from consumers, - check transaction status, and get account balance. + This module provides functions to: + - Request payments from consumers + - Request withdrawals from consumer accounts + - Check transaction status and account balance + - Get basic user information for account holders + - Validate account holder status """ alias MomoapiElixir.{Auth, Validator} @@ -91,6 +95,107 @@ defmodule MomoapiElixir.Collections do end end + @doc """ + Get basic user information for an account holder. + + Retrieve basic user information for a specific account holder using their + party ID type and party ID. + + ## Parameters + + - `config` - Configuration map with subscription_key, user_id, api_key, and target_environment + - `account_holder_id_type` - Type of account identifier (defaults to "MSISDN") + - "MSISDN" - Mobile phone number + - "EMAIL" - Email address + - `account_holder_id` - The account identifier (phone number or email) + + ## Examples + + iex> config = %{subscription_key: "key", user_id: "user", api_key: "api", target_environment: "sandbox"} + # Using default MSISDN type + iex> MomoapiElixir.Collections.get_basic_user_info(config, "256784123456") + {:ok, %{"given_name" => "John", "family_name" => "Doe"}} + + # Explicitly specifying type + iex> MomoapiElixir.Collections.get_basic_user_info(config, "MSISDN", "256784123456") + {:ok, %{"given_name" => "John", "family_name" => "Doe"}} + + # Using email + iex> MomoapiElixir.Collections.get_basic_user_info(config, "EMAIL", "user@example.com") + {:ok, %{"given_name" => "Jane", "family_name" => "Smith"}} + """ + @spec get_basic_user_info(config(), String.t(), String.t()) :: {:ok, map()} | {:error, term()} + @spec get_basic_user_info(config(), String.t()) :: {:ok, map()} | {:error, term()} + def get_basic_user_info(config, account_holder_id_type \\ "MSISDN", account_holder_id) do + with {:ok, token} <- Auth.get_token(:collections, config), + headers <- build_headers(token, config), + {:ok, response} <- @client.get("/collection/v1_0/accountholder/#{account_holder_id_type}/#{account_holder_id}/basicuserinfo", headers) do + handle_user_info_response(response) + end + end + + @doc """ + Request to withdraw money from a consumer account. + + This function allows you to request a withdrawal from a consumer's account. + The consumer will be asked to authorize the withdrawal. + + ## Examples + + iex> config = %{subscription_key: "key", user_id: "user", api_key: "api", target_environment: "sandbox"} + iex> withdraw_request = %{amount: "100", currency: "UGX", externalId: "withdraw_123", payer: %{partyIdType: "MSISDN", partyId: "256784123456"}} + iex> MomoapiElixir.Collections.request_to_withdraw(config, withdraw_request) + {:ok, "reference-id-uuid"} + """ + @spec request_to_withdraw(config(), map()) :: {:ok, String.t()} | {:error, term()} + def request_to_withdraw(config, body) do + with {:ok, validated_body} <- Validator.validate_collections(body), + {:ok, token} <- Auth.get_token(:collections, config), + {:ok, reference_id} <- generate_reference_id(), + headers <- build_headers(token, config, reference_id), + {:ok, response} <- @client.post("/collection/v1_0/requesttowithdraw", validated_body, headers) do + handle_payment_response(response, reference_id) + end + end + + @doc """ + Validate account holder status. + + Check if an account holder is active and able to receive transactions. + + ## Parameters + + - `config` - Configuration map with subscription_key, user_id, api_key, and target_environment + - `account_holder_id_type` - Type of account identifier (defaults to "MSISDN") + - "MSISDN" - Mobile phone number + - "EMAIL" - Email address + - `account_holder_id` - The account identifier (phone number or email) + + ## Examples + + iex> config = %{subscription_key: "key", user_id: "user", api_key: "api", target_environment: "sandbox"} + # Using default MSISDN type + iex> MomoapiElixir.Collections.validate_account_holder_status(config, "256784123456") + {:ok, %{"result" => true}} + + # Explicitly specifying type + iex> MomoapiElixir.Collections.validate_account_holder_status(config, "MSISDN", "256784123456") + {:ok, %{"result" => true}} + + # Using email + iex> MomoapiElixir.Collections.validate_account_holder_status(config, "EMAIL", "user@example.com") + {:ok, %{"result" => false}} + """ + @spec validate_account_holder_status(config(), String.t(), String.t()) :: {:ok, map()} | {:error, term()} + @spec validate_account_holder_status(config(), String.t()) :: {:ok, map()} | {:error, term()} + def validate_account_holder_status(config, account_holder_id_type \\ "MSISDN", account_holder_id) do + with {:ok, token} <- Auth.get_token(:collections, config), + headers <- build_headers(token, config), + {:ok, response} <- @client.get("/collection/v1_0/accountholder/#{account_holder_id_type}/#{account_holder_id}/active", headers) do + handle_account_status_response(response) + end + end + # Private functions defp generate_reference_id do @@ -135,6 +240,22 @@ defmodule MomoapiElixir.Collections do {:error, %{status_code: status_code, body: decode_body(body)}} end + defp handle_user_info_response(%{status_code: 200, body: body}) do + {:ok, decode_body(body)} + end + + defp handle_user_info_response(%{status_code: status_code, body: body}) do + {:error, %{status_code: status_code, body: decode_body(body)}} + end + + defp handle_account_status_response(%{status_code: 200, body: body}) do + {:ok, decode_body(body)} + end + + defp handle_account_status_response(%{status_code: status_code, body: body}) do + {:error, %{status_code: status_code, body: decode_body(body)}} + end + defp decode_body(""), do: %{} defp decode_body(body) when is_binary(body) do case Poison.decode(body) do