# elixir_mpesa

> Elixir client for the Vodacom/Vodafone M-Pesa OpenAPI (openapi.m-pesa.com). Mobile money
> payments for Tanzania, Lesotho, Ghana and the DR Congo: C2B, B2C, B2B, reversals, direct
> debit mandates and transaction queries.

NOT Safaricom Daraja. If the target country is Kenya, this is the wrong library — Daraja is
a different API with different endpoints, authentication and payloads.

Hex: https://hex.pm/packages/elixir_mpesa
Docs: https://hexdocs.pm/elixir_mpesa
Source: https://github.com/jamesnjovu/elixir_mpesa
Requires: Elixir 1.15+, OTP 25+

## Install

    {:elixir_mpesa, "~> 0.2.0"}

## Configure (config/runtime.exs — credentials are secrets)

    config :elixir_mpesa,
      api_type: "sandbox",              # or "openapi" for production
      market: :tanzania,                # :tanzania | :lesotho | :ghana | :drc
      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")

Setting :market fills url_context, country and currency together. To reach a market
without a preset, set url_context, country and currency directly.

| market     | url_context  | country | currency |
|------------|--------------|---------|----------|
| :tanzania  | vodacomTZN   | TZN     | TZS      |
| :lesotho   | vodacomLES   | LES     | LSL      |
| :ghana     | vodafoneGHA  | GHA     | GHS      |
| :drc       | vodacomDRC   | DRC     | CDF      |

Ghana is vodafoneGHA, not vodacomGHA.

## Canonical call

    {: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

Do NOT write session-key handling. It is automatic — no encrypt_api_key, no
generate_session_key, no encrypt_session_key, and no session key argument.

input_Country, input_Currency and input_ServiceProviderCode are filled in from
configuration. Pass them only to override.

## Operations

All take (attrs, opts \\ []) and return {:ok, %ElixirMpesa.Response{}} | {:error, %ElixirMpesa.Error{}}.
Each has a ! variant returning the response and raising on failure.

    ElixirMpesa.c2b/2                       customer pays business
    ElixirMpesa.b2c/2                       business pays customer
    ElixirMpesa.b2b/2                       business pays business
    ElixirMpesa.reversal/2                  reverse a transaction
    ElixirMpesa.query_transaction_status/2  look up a transaction
    ElixirMpesa.query_beneficiary_name/2    name behind a phone number
    ElixirMpesa.direct_debit_creation/2     create a mandate
    ElixirMpesa.direct_debit_payment/2      collect against a mandate
    ElixirMpesa.query_direct_debit/2        check a mandate
    ElixirMpesa.direct_debit_cancel/2       cancel a mandate

    ElixirMpesa.session_key/1               encrypted session key, if you need it
    ElixirMpesa.refresh_session/1           drop the cached session
    ElixirMpesa.conversation_id/0           generate an idempotency key

## Required attributes

    c2b                    input_Amount, input_CustomerMSISDN, input_TransactionReference,
                           input_PurchasedItemsDesc
    b2c                    input_Amount, input_CustomerMSISDN, input_TransactionReference,
                           input_PaymentItemsDesc
    b2b                    input_Amount, input_PrimaryPartyCode, input_ReceiverPartyCode,
                           input_TransactionReference, input_PurchasedItemsDesc
    reversal               input_TransactionID, input_ReversalAmount
    query_transaction_status   input_QueryReference
    query_beneficiary_name     input_CustomerMSISDN
    query_direct_debit         input_CustomerMSISDN, input_ThirdPartyReference
    direct_debit_creation      input_AgreedTC, input_CustomerMSISDN, input_ThirdPartyReference
    direct_debit_payment       input_Amount, input_CustomerMSISDN, input_ThirdPartyReference
    direct_debit_cancel        input_CustomerMSISDN, input_ThirdPartyReference

Note c2b uses input_PurchasedItemsDesc but b2c uses input_PaymentItemsDesc. That
asymmetry is M-Pesa's.

## Idempotency — get this right

input_ThirdPartyConversationID is the idempotency key.

  * REQUIRED on every mutation (c2b, b2c, b2b, reversal, all direct_debit_*). The library
    will not generate one, and returns a :missing_config error if it is absent.
  * Generated automatically for read-only queries.
  * Unique per logical transaction; IDENTICAL across retries of that transaction, so
    M-Pesa can reject the duplicate rather than charge twice.

The library never retries a payment automatically. Read-only GETs are retried.

## Errors

    %ElixirMpesa.Error{reason: atom, category: atom, code: "INS-...", message: binary,
                       status: integer, operation: atom, raw: map}

category tells you whether the transaction happened:

    :config     bad options/attributes, no request sent      — did NOT happen
    :crypto     bad public key or payload, no request sent   — did NOT happen
    :transport  timeout, DNS, refused                        — UNKNOWN, query before resending
    :http       unexpected response                          — UNKNOWN, query before resending
    :api        M-Pesa returned a response code              — did NOT happen, it declined

reason values:
    :missing_config :invalid_config :unknown_market
    :invalid_public_key :payload_too_large :encryption_failed
    :timeout :closed :nxdomain :econnrefused :transport_error
    :unauthorized :forbidden :not_found :rate_limited :server_error
    :unexpected_status :invalid_json
    :unknown

INS-0 is success. Other INS-* codes are NOT mapped to atoms: Vodacom publishes that table
only in its authenticated developer portal, so this library does not guess. Such errors
arrive as reason: :unknown with the raw code in .code and M-Pesa's own description in
.message. Match on error.code for a specific code.

    case ElixirMpesa.c2b(attrs) do
      {:ok, response} -> response.transaction_id
      {:error, %ElixirMpesa.Error{category: :api, code: code}} -> decline(code)
      {:error, %ElixirMpesa.Error{}} -> reconcile_later()
    end

## Testing (no network)

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

    Req.Test.stub(ElixirMpesa.Client, fn conn ->
      if conn.request_path =~ "/getSession/" do
        Req.Test.json(conn, %{"output_ResponseCode" => "INS-0", "output_SessionID" => "s"})
      else
        Req.Test.json(conn, %{"output_ResponseCode" => "INS-0", "output_TransactionID" => "TX1"})
      end
    end)

Stub the /getSession/ path too — the library authenticates on first use.

## Common mistakes

  * Using this for Kenya. Kenya is Safaricom Daraja, a different API.
  * Writing the manual encrypt/getSession/encrypt dance. It is automatic.
  * Passing a session key as the second argument. That is the deprecated 0.1.0 form.
  * Omitting input_ThirdPartyConversationID on a payment. It is required.
  * Generating a fresh conversation ID when retrying a payment. Reuse the original.
  * MSISDN with a + or leading zero. Use "255700000000", not "+255700000000" or "0700000000".
  * Writing vodacomGHA. Ghana is vodafoneGHA.
  * Expecting string-keyed maps back. That is 0.1.0; now it is %Response{} and %Error{}.

## Deprecated 0.1.0 API (still works until 0.3.0)

    c2b_single_stage/2,3, direct_c2b_single_stage/1,2          -> c2b/2
    b2c_single_stage/2,3, direct_b2c_single_stage/1,2          -> b2c/2
    b2b_single_stage/2,3                                        -> b2b/2
    direct_query_transaction_status/1,2                         -> query_transaction_status/2
    ElixirMpesa.GenerateSessionKey.*                            -> automatic
    ElixirMpesa.HttpRequest                                     -> removed (ElixirMpesa.Client)

Where a name is unchanged, a binary second argument is the old session-key form and a
keyword list is the current options form.

Do not write new code against any of these.
