ElixirMpesa.Error exception (ElixirMpesa v0.2.0)

View Source

A single, structured error type for every failure this library can produce.

Every ElixirMpesa function returns either {:ok, ElixirMpesa.Response.t()} or {:error, ElixirMpesa.Error.t()}. Match on :reason — it is a documented atom, stable across releases — rather than on the raw M-Pesa response code string.

case ElixirMpesa.c2b(attrs) do
  {:ok, response} -> {:ok, response.transaction_id}
  {:error, %ElixirMpesa.Error{reason: :insufficient_balance}} -> :ask_customer_to_top_up
  {:error, %ElixirMpesa.Error{reason: :duplicate_transaction}} -> :already_processed
  {:error, %ElixirMpesa.Error{} = error} -> Logger.error(Exception.message(error))
end

Fields

  • :reason - a documented atom. See reason/0.
  • :category - :config, :crypto, :transport, :http or :api. Useful for deciding whether a failure is worth retrying at all.
  • :code - the raw output_ResponseCode from M-Pesa (for example "INS-2006"), or nil for failures that never reached the API.
  • :message - a human-readable description.
  • :status - the HTTP status code, when there was a response.
  • :operation - the library function that failed, e.g. :c2b.
  • :raw - the full decoded response body, so nothing is ever lost.

This is an exception, so it can be raised — the ! variants such as ElixirMpesa.c2b!/2 do exactly that.

Summary

Types

The stable, matchable failure reason.

t()

Functions

Builds an error from a decoded M-Pesa response body.

Whether an error means the session key is no longer valid and should be refreshed.

Types

category()

@type category() :: :config | :crypto | :transport | :http | :api

reason()

@type reason() ::
  :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
  | :insufficient_balance
  | :invalid_msisdn
  | :duplicate_transaction
  | :session_expired
  | :invalid_api_key
  | :transaction_failed
  | :unknown

The stable, matchable failure reason.

Configuration and crypto failures happen before any request is sent. Transport and HTTP failures mean the request left the machine but no M-Pesa response code came back. API failures carry a :code.

:unknown means M-Pesa returned a response code this library does not have a mapping for — :code and :raw still hold the original values.

t()

@type t() :: %ElixirMpesa.Error{
  __exception__: true,
  category: category(),
  code: String.t() | nil,
  message: String.t(),
  operation: atom() | nil,
  raw: map() | String.t() | nil,
  reason: reason(),
  status: pos_integer() | nil
}

Functions

from_response(body, opts \\ [])

@spec from_response(
  map(),
  keyword()
) :: t()

Builds an error from a decoded M-Pesa response body.

M-Pesa reports failures in "output_ResponseCode" with a human description in "output_ResponseDesc". The description is used verbatim as the error message — this library does not paraphrase it.

Codes without a known mapping become :unknown; :code and :raw still carry the original values, so error.code == "INS-2006" remains matchable.

Examples

iex> body = %{"output_ResponseCode" => "INS-995", "output_ResponseDesc" => "Customer profile problem"}
iex> error = ElixirMpesa.Error.from_response(body, status: 400)
iex> {error.reason, error.code, error.category}
{:unknown, "INS-995", :api}
iex> Exception.message(error)
"Customer profile problem (INS-995)"

new(reason, category, message, opts \\ [])

@spec new(reason(), category(), String.t(), keyword()) :: t()

Builds an error.

Examples

iex> error = ElixirMpesa.Error.new(:timeout, :transport, "the request timed out")
iex> error.reason
:timeout
iex> Exception.message(error)
"the request timed out"

session_expired?(error)

@spec session_expired?(t()) :: boolean()

Whether an error means the session key is no longer valid and should be refreshed.

ElixirMpesa.Session uses this to decide whether to re-authenticate and retry once.

Examples

iex> ElixirMpesa.Error.session_expired?(ElixirMpesa.Error.new(:unauthorized, :http, "nope"))
true

iex> ElixirMpesa.Error.session_expired?(ElixirMpesa.Error.new(:timeout, :transport, "nope"))
false