lib/kora_pay/client.ex

defmodule KoraPay.Client do
  @moduledoc """
    Create a connection to the [korapay api](https://docs.korapay.com/),
    and query it. Some routes may require different authentication types, ensure
    the correct keys are supplied in `./config` before starting the application.

    see: https://korahq.atlassian.net/wiki/spaces/AR/pages/733970455/Authentication

    You may wish to verify a client build by querying the api in an `iex` session:

    ```
      iex(1)> KoraPay.Client.get_balances()
      iex(2)> {:ok, %{"NGN" => %{"available_balance" => 0, "pending_balance" => 0}}}
    ```

    Do not consume the client directly, use the [public interface](./KoraPay.html) in your application:
    ```
    defmodule MyApp do
      def print_balance do
        case KoraPay.balances() do
          {:ok, balance} <- IO.inspect(balance)
          {:error, error} <- IO.inspect(error)
        end
      end
    end
    ```
  """
  alias KoraPay.Behaviour
  @behaviour Behaviour

  @version "v1"
  @namespace "merchant/api"


  @spec build_client(:public | :private) :: Tesla.Client.t()
  def build_client(auth_type) do
    middleware = [
      {Tesla.Middleware.BaseUrl, "https://api.korapay.com/#{@namespace}/#{@version}"},
      {Tesla.Middleware.BearerAuth, token: Application.get_env(:kora_pay, auth_type)},
      Tesla.Middleware.JSON
    ]

    Tesla.client(middleware)
  end

  @impl Behaviour
  def create_charge(opts) do
    build_client(:private)
    |> Tesla.post("/charges/initialize", opts)
    |> parse_response()
  end

  @impl Behaviour
  def charge_status(ref) do
    build_client(:private)
    |> Tesla.get("/charges/#{ref}")
    |> parse_response()
  end

  @impl Behaviour
  def charge_card(opts) do
    build_client(:private)
    |> Tesla.post("/charges/card", opts)
    |> parse_response()
  end

  @impl Behaviour
  def authorize_charge(opts) do
    build_client(:private)
    |> Tesla.post("/charges/card/authorize", opts)
    |> parse_response()
  end

  @impl Behaviour
  def disburse(opts) do
    build_client(:private)
    |> Tesla.post("/transactions/disburse", opts)
    |> parse_response()
  end

  @impl Behaviour
  def verify_disbursement(txn_ref) do
    build_client(:private)
    |> Tesla.get("/transactions/#{txn_ref}")
    |> parse_response()
  end

  @impl Behaviour
  def transactions do
    build_client(:private)
    |> Tesla.get("/transactions")
    |> parse_response()
  end

  @impl Behaviour
  def resolve_bank_account(opts) do
    build_client(:public)
    |> Tesla.post("/misc/banks/resolve", opts)
    |> parse_response()
  end

  @impl Behaviour
  def list_banks do
    build_client(:public)
    |> Tesla.get("/misc/banks")
    |> parse_response()
  end

  @impl Behaviour
  def balances do
    build_client(:private)
    |> Tesla.get("/balances")
    |> parse_response()
  end

  @impl Behaviour
  def create_virtual_bank_account(opts) do
    build_client(:private)
    |> Tesla.post("/virtual-bank-account", opts)
    |> parse_response()
  end

  @impl Behaviour
  def virtual_bank_account_details(ref) do
    build_client(:private)
    |> Tesla.get("/virtual-bank-account/#{ref}")
    |> parse_response()
  end

  @impl Behaviour

  def virtual_bank_account_transactions(opts) do
    build_client(:private)
    |> Tesla.get("/virtual-bank-account/transactions", body: opts)
    |> parse_response()
  end

  defp parse_response(request) do
    case request do
      {:ok, %{status: 200, body: %{"status" => true} = body}} ->
        {:ok, body["data"]}

      response ->
        parse_error(response)
    end
  end

  defp parse_error(response) do
    case response do
      {:ok, %{status: 400, body: body}} ->
        {:error, %{reason: body["error"], details: body["message"]}}

      {:ok, %{status: 401, body: body}} ->
        {:error, %{reason: body["error"], details: body["message"]}}

      {:ok, %{status: 404, body: body}} ->
        {:error, %{reason: body["error"], details: body["message"]}}

      {:ok, %{status: 403, body: body}} ->
        {:error, %{reason: body["error"], details: body["message"]}}

      {:ok, %{status: 500, body: body}} ->
        {:error, %{reason: "Internal server error", details: body["message"]}}

      {:ok, %{status: status, body: body}} ->
        {:error, %{reason: "unexpected error - #{status}", details: body["message"]}}

      _ ->
        {:error, %{reason: "unexpected error", details: "unknown"}}
    end
  end
end