Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 35 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,26 +25,48 @@ Your user id is 8bf4101b-ee3a-4eb1-968d-73dfc94e4011 and your API key is 6d18ef4

```

### Collections
The collections' api can be started with the following parameters. Note that the user Id and api key for production are provided on the MTN OVA dashboard;
### Configuration

- `subscription_key`: Primary Key for the Collections product.
- `user_id`: For sandbox, use the one generated with the `mix provision` command.
- `api_key`: For sandbox, use the one generated with the `mix provision` command.
The library supports multiple ways to configure your MTN MoMo API credentials:

#### Option 1: Environment Variables (Recommended for Production)

Set these environment variables:

```bash
export MOMO_SUBSCRIPTION_KEY="your_subscription_key"
export MOMO_USER_ID="your_user_id"
export MOMO_API_KEY="your_api_key"
export MOMO_TARGET_ENVIRONMENT="production" # or "sandbox"
```

Then use in your code:

```elixir
alias MomoapiElixir.Collection
# Create options. Subscription key, user id and api key are required
options = %Collection.Option{
subscription_key: "some_key",
user_id: "some_user_id",
api_key: "some_api_key",
}
# Automatically loads from environment variables
{:ok, config} = MomoapiElixir.Config.from_env()

# Use with Collections API
{:ok, reference_id} = MomoapiElixir.Collections.request_to_pay(config, payment_body)
```

Collection.start(options)
#### Option 2: Manual Configuration

```elixir
# Create configuration manually
config = %{
subscription_key: "your_subscription_key",
user_id: "your_user_id",
api_key: "your_api_key",
target_environment: "sandbox" # or "production"
}

# Use with Collections API
{:ok, reference_id} = MomoapiElixir.Collections.request_to_pay(config, payment_body)
```

### Collections

#### Functions
- `request_to_pay(body)` This operation is used to request a payment from a consumer (Payer). The payer will be asked to authorize the payment. The transaction is executed once the payer has authorized the payment. The transaction will be in status PENDING until it is authorized or declined by the payer, or it is timed out by the system. Status of the transaction can be validated by using get_transaction_status

Expand Down
9 changes: 6 additions & 3 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use Mix.Config
import Config

config :momoapi_elixir, http_client: MomoapiElixir.Client
# Base configuration - environment-specific configs will override these
config :momoapi_elixir,
http_client: MomoapiElixir.Client

import_config "#{Mix.env()}.exs"
# Import environment-specific configuration
import_config "#{config_env()}.exs"
6 changes: 6 additions & 0 deletions config/dev.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Config

# Development-specific configuration
config :momoapi_elixir,
base_url: "https://sandbox.momodeveloper.mtn.com",
target_environment: "sandbox"
24 changes: 24 additions & 0 deletions config/prod.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Config

# Production configuration
config :momoapi_elixir,
base_url: "https://momoapi.mtn.com",
target_environment: "production"

# OPTION 1: Environment Variables (Recommended)
# Set these environment variables in your production environment:
# export MOMO_SUBSCRIPTION_KEY="your_production_subscription_key"
# export MOMO_USER_ID="your_production_user_id"
# export MOMO_API_KEY="your_production_api_key"
# export MOMO_TARGET_ENVIRONMENT="production"
#
# Then use: {:ok, config} = MomoapiElixir.Config.from_env()

# OPTION 2: Application Config (Alternative)
# Uncomment and set these if you prefer config-based credentials:
# config :momoapi_elixir,
# subscription_key: System.get_env("MOMO_SUBSCRIPTION_KEY"),
# user_id: System.get_env("MOMO_USER_ID"),
# api_key: System.get_env("MOMO_API_KEY")
#
# Then use: {:ok, config} = MomoapiElixir.Config.from_app_config()
8 changes: 6 additions & 2 deletions config/test.exs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
use Mix.Config
import Config

config :momoapi_elixir, http_client: ClientMock
# Test-specific configuration
config :momoapi_elixir,
http_client: ClientMock,
base_url: "https://sandbox.momodeveloper.mtn.com",
target_environment: "sandbox"
105 changes: 101 additions & 4 deletions lib/momoapi_elixir.ex
Original file line number Diff line number Diff line change
@@ -1,9 +1,106 @@
defmodule MomoapiElixir do
use Application
@moduledoc """
MTN Mobile Money API client for Elixir.

@moduledoc false
This library provides a functional interface to interact with MTN's Mobile Money API,
supporting both Collections (payments from consumers) and Disbursements (transfers to payees).

def start(_type, _args) do
# MomoapiElixir.Supervisor.start_link()
## Quick Start

# Option 1: Use environment variables (Recommended for production)
{:ok, config} = MomoapiElixir.Config.from_env()

# Option 2: Manual configuration
config = %{
subscription_key: "your_subscription_key",
user_id: "your_user_id",
api_key: "your_api_key",
target_environment: "sandbox" # or "production"
}

# Collections - Request payment from consumer
payment = %{
amount: "100",
currency: "UGX",
externalId: "payment_123",
payer: %{
partyIdType: "MSISDN",
partyId: "256784123456"
},
payerMessage: "Payment for goods",
payeeNote: "Thank you"
}

{:ok, reference_id} = MomoapiElixir.Collections.request_to_pay(config, payment)

# Disbursements - Transfer money to payee
transfer = %{
amount: "50",
currency: "UGX",
externalId: "transfer_456",
payee: %{
partyIdType: "MSISDN",
partyId: "256784123456"
}
}

{:ok, reference_id} = MomoapiElixir.Disbursements.transfer(config, transfer)

## Modules

- `MomoapiElixir.Collections` - Collections API functions
- `MomoapiElixir.Disbursements` - Disbursements API functions
- `MomoapiElixir.Auth` - Authentication utilities
- `MomoapiElixir.Validator` - Request validation utilities
"""

# Convenience aliases for the main APIs
alias MomoapiElixir.{Collections, Disbursements}

@type config :: %{
subscription_key: String.t(),
user_id: String.t(),
api_key: String.t(),
target_environment: String.t()
}

@doc """
Request a payment from a consumer (Collections API).

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()}
def request_to_pay(config, body) do
Collections.request_to_pay(config, body)
end

@doc """
Transfer money to a payee (Disbursements API).

This is a convenience function that delegates to `MomoapiElixir.Disbursements.transfer/2`.
"""
@spec transfer(config(), map()) :: {:ok, String.t()} | {:error, term()}
def transfer(config, body) do
Disbursements.transfer(config, body)
end

@doc """
Get Collections account balance.

This is a convenience function that delegates to `MomoapiElixir.Collections.get_balance/1`.
"""
@spec get_collections_balance(config()) :: {:ok, map()} | {:error, term()}
def get_collections_balance(config) do
Collections.get_balance(config)
end

@doc """
Get Disbursements account balance.

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
end
91 changes: 69 additions & 22 deletions lib/momoapi_elixir/auth.ex
Original file line number Diff line number Diff line change
@@ -1,40 +1,87 @@
defmodule MomoapiElixir.Auth do
@moduledoc false
@moduledoc """
Authentication module for MTN Mobile Money API.

use HTTPoison.Base
@base_url Application.get_env(:momoapi_elixir, :base_url) || "https://sandbox.momodeveloper.mtn.com"
Handles token generation for both Collections and Disbursements APIs.
"""

def process_request_url(url) do
@base_url <> url
@client Application.compile_env(:momoapi_elixir, :http_client, MomoapiElixir.Client)

@type config :: %{
subscription_key: String.t(),
user_id: String.t(),
api_key: String.t()
}

@type service :: :collections | :disbursements

@doc """
Get an access token for the specified service.

## Examples

iex> config = %{subscription_key: "key", user_id: "user", api_key: "api"}
iex> MomoapiElixir.Auth.get_token(:collections, config)
{:ok, "access_token_string"}
"""
@spec get_token(service(), config()) :: {:ok, String.t()} | {:error, term()}
def get_token(:collections, config) do
authorize_service("/collection/token/", config)
end

def authorise_collections(%{subscription_key: subscription_key} = config) do
basic_auth_token = create_basic_auth_token(config)
headers = [
{"Authorization", "Basic #{basic_auth_token}"},
{"Ocp-Apim-Subscription-Key", subscription_key}
]
case post("/collection/token/", [], headers) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
%{"access_token" => access_token} = Poison.decode!(body)
access_token
end
def get_token(:disbursements, config) do
authorize_service("/disbursement/token/", config)
end

def authorise_disbursements(%{subscription_key: subscription_key} = config) do
# Private functions

defp authorize_service(endpoint, %{subscription_key: subscription_key} = config) do
basic_auth_token = create_basic_auth_token(config)
headers = [
{"Authorization", "Basic #{basic_auth_token}"},
{"Ocp-Apim-Subscription-Key", subscription_key}
]
case post("/disbursement/token/", [], headers) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
%{"access_token" => access_token} = Poison.decode!(body)
access_token

case @client.post(endpoint, %{}, headers) do
{:ok, %{status_code: 200, body: body}} ->
case decode_token_response(body) do
{:ok, token} -> {:ok, token}
{:error, reason} -> {:error, {:token_decode_error, reason}}
end

{:ok, %{status_code: status_code, body: body}} ->
{:error, {:auth_failed, status_code, decode_body(body)}}

{:error, reason} ->
{:error, {:http_error, reason}}
end
end

def create_basic_auth_token(%{user_id: user_id, api_key: api_key}) do
defp create_basic_auth_token(%{user_id: user_id, api_key: api_key}) do
Base.encode64("#{user_id}:#{api_key}")
end

defp decode_token_response(body) when is_binary(body) do
case Poison.decode(body) do
{:ok, %{"access_token" => access_token}} when is_binary(access_token) ->
{:ok, access_token}
{:ok, response} ->
{:error, {:missing_access_token, response}}
{:error, reason} ->
{:error, {:json_decode_error, reason}}
end
end

defp decode_token_response(body) do
{:error, {:invalid_response_format, body}}
end

defp decode_body(""), do: %{}
defp decode_body(body) when is_binary(body) do
case Poison.decode(body) do
{:ok, decoded} -> decoded
{:error, _} -> body
end
end
defp decode_body(body), do: body
end
Loading