# ElixirMpesa

[![Hex.pm](https://img.shields.io/hexpm/v/elixir_mpesa.svg)](https://hex.pm/packages/elixir_mpesa)
[![Hex Docs](https://img.shields.io/badge/hex-docs-blue.svg)](https://hexdocs.pm/elixir_mpesa)
[![CI](https://github.com/jamesnjovu/elixir_mpesa/actions/workflows/ci.yml/badge.svg)](https://github.com/jamesnjovu/elixir_mpesa/actions/workflows/ci.yml)
[![Downloads](https://img.shields.io/hexpm/dt/elixir_mpesa.svg)](https://hex.pm/packages/elixir_mpesa)
[![License](https://img.shields.io/hexpm/l/elixir_mpesa.svg)](https://github.com/jamesnjovu/elixir_mpesa/blob/main/LICENSE)

**M-Pesa mobile money payments for Elixir.** A client for the Vodacom/Vodafone **M-Pesa
OpenAPI**, covering customer-to-business (C2B), business-to-customer (B2C),
business-to-business (B2B), reversals, direct debit mandates and transaction queries
across **Tanzania, Lesotho, Ghana and the DR Congo**.

> [!IMPORTANT]
> This is the **Vodacom M-Pesa OpenAPI** (`openapi.m-pesa.com`). If you are integrating
> M-Pesa in **Kenya**, you need Safaricom's **Daraja** API and a different library —
> the endpoints, authentication and payloads are unrelated.

```elixir
{:ok, response} = ElixirMpesa.c2b(%{
  "input_Amount" => "10",
  "input_CustomerMSISDN" => "255700000000",
  "input_TransactionReference" => "INV-1024",
  "input_ThirdPartyConversationID" => ElixirMpesa.conversation_id(),
  "input_PurchasedItemsDesc" => "Order 1024"
})

response.transaction_id
#=> "49XCD123F6"
```

No session handshake, no country and currency on every call, no string-keyed error maps.

## Installation

```elixir
def deps do
  [{:elixir_mpesa, "~> 0.2.0"}]
end
```

Requires **Elixir 1.15+** and OTP 25+.

## Configuration

Credentials are secrets — put them in `config/runtime.exs`, read from the environment.

```elixir
import Config

config :elixir_mpesa,
  api_type: "sandbox",                 # "openapi" for production
  market: :tanzania,
  service_provider_code: System.get_env("MPESA_SERVICE_PROVIDER_CODE"),
  api_key: System.fetch_env!("MPESA_API_KEY"),
  public_key: System.fetch_env!("MPESA_PUBLIC_KEY")
```

Get credentials from the [M-Pesa OpenAPI Portal](https://openapiportal.m-pesa.com).

## Supported markets

Setting `:market` fills in the URL context, country code and currency together, so they
cannot drift apart.

| `:market` | Country | URL context | Country code | Currency |
|---|---|---|---|---|
| `:tanzania` | Tanzania | `vodacomTZN` | `TZN` | `TZS` |
| `:lesotho` | Lesotho | `vodacomLES` | `LES` | `LSL` |
| `:ghana` | Ghana | `vodafoneGHA` | `GHA` | `GHS` |
| `:drc` | DR Congo | `vodacomDRC` | `DRC` | `CDF` |

A market without a preset works too — set `url_context`, `country` and `currency`
directly. Sessions are cached per market, so one application can serve several countries
at once. See the [Markets guide](https://hexdocs.pm/elixir_mpesa/markets.html).

## Operations

| Function | Operation |
|---|---|
| `c2b/2` | Customer pays your business |
| `b2c/2` | Your business pays a customer — refunds, payouts, salaries |
| `b2b/2` | Your business pays another business |
| `reversal/2` | Reverse a completed transaction |
| `query_transaction_status/2` | Look up a transaction |
| `query_beneficiary_name/2` | Look up the name behind a phone number |
| `direct_debit_creation/2` | Create a mandate |
| `direct_debit_payment/2` | Collect against a mandate |
| `query_direct_debit/2` | Check a mandate |
| `direct_debit_cancel/2` | Cancel a mandate |

Each has a `!` variant that returns the response directly and raises on failure.

## Sessions are handled for you

The OpenAPI requires a session key obtained by encrypting your API key, exchanging it at
`getSession`, then encrypting the result. `ElixirMpesa.Session` does this on first use and
then caches the key per market, refreshes it before its one-hour expiry, collapses
concurrent cache misses into a single `getSession` call, and re-authenticates once if
M-Pesa rejects it mid-flight.

You can still drive it manually — see
[Authentication](https://hexdocs.pm/elixir_mpesa/authentication.html).

## Error handling

Every function returns `{:ok, ElixirMpesa.Response.t()}` or
`{:error, ElixirMpesa.Error.t()}`. Match on `reason` and `category`:

```elixir
case ElixirMpesa.c2b(attrs) do
  {:ok, response} ->
    confirm(response.transaction_id)

  # M-Pesa gave a definite answer: the money did not move.
  {:error, %ElixirMpesa.Error{category: :api, code: code}} ->
    decline(code)

  # Timeout or gateway failure — the outcome is unknown. Query, do not resend.
  {:error, %ElixirMpesa.Error{category: category}} when category in [:transport, :http] ->
    reconcile_later()
end
```

See [Error codes](https://hexdocs.pm/elixir_mpesa/error-codes.html).

## Retrying safely

`"input_ThirdPartyConversationID"` is the idempotency key. Generate one per transaction
with `ElixirMpesa.conversation_id/0` and reuse the **same** one when retrying that
transaction — M-Pesa uses it to reject the duplicate rather than charge twice.

This is why the library never retries a payment automatically, and refuses to generate a
conversation ID for one. Read-only queries get one generated.

## Testing

Built on [Req](https://hexdocs.pm/req), so your suite runs against a stub with no network:

```elixir
config :elixir_mpesa, req_options: [plug: {Req.Test, ElixirMpesa.Client}]
```

```elixir
Req.Test.stub(ElixirMpesa.Client, fn conn ->
  Req.Test.json(conn, %{"output_ResponseCode" => "INS-0", "output_TransactionID" => "TX1"})
end)
```

See the [Testing guide](https://hexdocs.pm/elixir_mpesa/testing.html).

## Documentation

- [Getting started](https://hexdocs.pm/elixir_mpesa/getting-started.html)
- [Markets](https://hexdocs.pm/elixir_mpesa/markets.html)
- [Authentication](https://hexdocs.pm/elixir_mpesa/authentication.html)
- [Payments](https://hexdocs.pm/elixir_mpesa/payments.html)
- [Direct debit](https://hexdocs.pm/elixir_mpesa/direct-debit.html)
- [Error codes](https://hexdocs.pm/elixir_mpesa/error-codes.html)
- [Testing](https://hexdocs.pm/elixir_mpesa/testing.html)
- [Upgrading from 0.1.0](https://hexdocs.pm/elixir_mpesa/upgrading.html)

Using an AI coding assistant? [`llms.txt`](https://hexdocs.pm/elixir_mpesa/llms.txt) is a
condensed, machine-readable summary of the whole API.

## Upgrading from 0.1.0

0.1.0 code keeps working — the old functions remain as deprecated shims until 0.3.0. You
should upgrade regardless: **0.1.0 disabled TLS certificate verification on every
request**, and crashed rather than returning an error tuple on several ordinary HTTP
statuses. See [Upgrading](https://hexdocs.pm/elixir_mpesa/upgrading.html).

## Contributing

Issues and pull requests welcome at
[github.com/jamesnjovu/elixir_mpesa](https://github.com/jamesnjovu/elixir_mpesa).

One contribution would be especially valuable: **the `INS-*` response code table**.
Vodacom publishes it only inside the authenticated developer portal, so this library
deliberately does not guess at code meanings. If you have that documentation, adding it to
`ElixirMpesa.Error` would help everyone.

```bash
mix deps.get
mix test          # or: mix ci  — format, credo, dialyzer and tests
```

## License

MIT — see [LICENSE](LICENSE).
