lib/kora_pay.ex

defmodule KoraPay do
  @moduledoc """
  KoraPay client wrapper (https://docs.korapay.com/).

  ## Installation
  Set the the required environment variables for production.
  For development/sandbox data set `config/dev.exs`.

  see: https://developers.korapay.com/docs/api-keys

  ```
    config :kora_pay,
      public: "test_yourkey",
      private: "test_yourkey",
      webhook_url: "https://www.myserver.com/webhook",
  ```

  ## Usage
  ```
    defmodule MyApp do
      def all_banks do
        case KoraPay.list_banks() do
          {:ok, banks} -> IO.inspect(banks)
          {:error, error} -> IO.inspect(error)
        end
      end
    end
  ```
  """

  ### Entities ###
  @type card :: %{
          card_type: :mastercard | :visa | :verve,
          first_six: String.t(),
          last_four: String.t(),
          expiry: String.t()
        }

  @type transaction :: %{
          reference: String.t(),
          status: status(),
          type: transaction_type(),
          amount: float(),
          fee: float(),
          narration: String.t(),
          currency: String.t(),
          created_at: DateTime.t(),
          transaction_status: String.t(),
          callback_url: String.t(),
          meta: %{},
          customer: customer()
        }

  @type virtual_bank_account_txn :: %{
          reference: String.t(),
          status: status(),
          amount: String.t(),
          fee: String.t(),
          currency: String.t(),
          description: String.t(),
          payer_bank_account: payer_bank_account()
        }

  @type customer :: %{
          required(:email) => String.t(),
          optional(:name) => String.t(),
          optional(:phone) => String.t()
        }

  @type destination :: %{
          type: String.t(),
          amount: float(),
          currency: String.t(),
          narration: String.t(),
          bank_account: short_bank_account(),
          customer: customer()
        }

  @type disbursement :: %{
          amount: non_neg_integer(),
          fee: float(),
          currency: String.t(),
          status: status(),
          reference: String.t(),
          narration: String.t(),
          customer: customer()
        }

  @type charge_reference :: %{reference: String.t(), checkout_url: String.t()}

  # Auth
  @type auth_model :: :otp | :three_ds | :avs | :pin

  # Transaction types
  @type balance :: %{
          NGN: %{
            pending_balance: float(),
            available_balance: float()
          }
        }

  @type status :: :success | :pending | :processing | :expired | :failed

  @type transaction_type :: :collection | :disubursement

  @type channel :: :card | :bank_transfer

  ### Bank Account Types ###
  @type bank_account :: %{
          bank_name: String.t(),
          bank_code: String.t(),
          account_number: String.t(),
          account_name: String.t()
        }

  @type virtual_account :: %{
          account_reference: String.t(),
          unique_id: String.t(),
          account_status: String.t(),
          created_at: DateTime.t(),
          currency: String.t(),
          bank_account: bank_account(),
          customer: customer()
        }

  @type payer_bank_account :: %{
          account_number: String.t(),
          account_name: String.t(),
          bank_name: String.t()
        }

  @type short_bank_account :: %{
          bank: String.t(),
          account: String.t()
        }

  @type misc_bank_account :: %{
          code: String.t(),
          country: String.t(),
          name: String.t(),
          nibss_bank_code: String.t(),
          slug: String.t()
        }

  ### API Types ###
  @type charge_response :: %{
          amount: non_neg_integer(),
          amount_charged: non_neg_integer(),
          auth_model: auth_model(),
          currency: String.t(),
          fee: float(),
          vat: float(),
          response_message: String.t(),
          payment_reference: String.t(),
          status: status(),
          transaction_reference: String.t(),
          authorization: %{},
          card: card()
        }

  @type disbursement_status :: %{
          type: transaction_type(),
          transaction_status: String.t(),
          transaction_date: DateTime.t(),
          channel: channel(),
          disbursement: disbursement()
        }

  @type charge_status :: %{
          reference: String.t(),
          amount: non_neg_integer(),
          fee: float(),
          currency: String.t(),
          status: status(),
          description: String.t(),
          created_at: DateTime.t(),
          payer_bank_account: payer_bank_account(),
          card: card()
        }

  @type virtual_bank_account_txn_response :: %{
          total_amount_received: 1000,
          account_number: String.t(),
          currency: String.t(),
          transactions: [virtual_bank_account_txn()],
          pagination: %{
            page: integer(),
            total: integer(),
            pageCount: integer(),
            totalPages: integer()
          }
        }

  ### Helper Types
  @type charge_options :: %{
          optional(:redirect_url) => String.t(),
          optional(:channels) => [channel()],
          optional(:default_channel) => channel()
        }

  @type auth_options ::
          String.t()
          | %{
              state: String.t(),
              city: String.t(),
              country: String.t(),
              address: String.t(),
              zip_codes: String.t()
            }

  @type error :: {:error, %{reason: String.t(), details: %{}}}

  @doc """
    Create a generic payment.

    see: https://docs.korapay.com/#f192d2e8-aab2-4f5a-98ef-fa0ed7e2d853

  ## Examples
  ```
  iex(1)> KoraPay.create_charge(1000, "NGN", "test-xyz", %{email: "jycdmbhw@sharklasers.com"})
  {:ok, %{
      "checkout_url" => "https://test-checkout.korapay.com/test-txn/pay",
      "reference" => "test-txn" \# reference is automatically generated by library.
    }
  }
  ```
  ## Charge options
    1. `reference` : transaction reference, if not provided, will be auto generated.
    2. `charge_options`: map of zero or more attributes.
    - `:redirect_url`: URL to redirect your customer when the transaction is complete.
    - `:default_channel`: channel that shows up when client modal is instantiated. E.g `"bank_transfer"`
    - `:channels`: Allowed payment channels for this transaction e.g `["card", "bank_transfer"]`
  """

  @spec create_charge(
          non_neg_integer(),
          String.t(),
          String.t(),
          customer(),
          String.t(),
          charge_options()
        ) :: charge_response() | error()
  def create_charge(
        amount,
        currency,
        narration,
        customer,
        reference \\ generate_reference(),
        charge_options \\ %{}
      ) do
    params =
      Map.merge(
        %{
          amount: to_string(amount),
          currency: currency,
          reference: reference,
          notification_url: Application.get_env(:kora_pay, :webhook_url),
          narration: narration,
          customer: customer
        },
        charge_options
      )

    impl().create_charge(params)
  end

  @doc """
  Find the details of a charge by providing a reference.

  ## Examples
  ```
  iex(1)> KoraPay.charge_status("test-txn")
  {:ok, %{
          "amount" => "1000.00",
          "currency" => "NGN",
          "description" => "Fix Test Webhook",
          "fee" => nil,
          "reference" => "test-txn",
          "status" => "processing"
          }
    }
  ```
  """
  @spec charge_status(String.t()) :: charge_status() | error()
  def charge_status(reference), do: impl().charge_status(reference)

  @doc """
  Authorize a [charge](./KoraPay.html#charge_card/1) that is `:processing`.

  ## Examples
  ```
  iex(1)> KoraPay.authorize_charge("test-txn", :otp, "12345")
  iex(2)> KoraPay.authorize_charge("test-txn", :pin, "1234")
  iex(3)> KoraPay.authorize_charge("test-txn", :avs, %{state: "Lagos", city: "Lekki", ...})
  ```

  ## Options
    1. Required only if auth type is `:pin`
    2. Required only if auth type is `:otp`
    3. Required only if auth type is `:avs`
      - state
      - city
      - country
      - address
      - zip_codes
  """
  @spec authorize_charge(String.t(), auth_model(), auth_options()) ::
          charge_response() | error()
  def authorize_charge(txn_reference, auth_model, options) do
    auth =
      case auth_model do
        :avs -> %{avs: options}
        :otp -> %{otp: options}
        :pin -> %{pin: options}
      end

    params = %{
      transaction_reference: txn_reference,
      authorization: auth
    }

    impl().authorize_charge(params)
  end

  @doc """
  Create a charge on a card.

  ## Examples
  ```
  enc_data = "test_encrypted_base64_data"

  iex(1)> KoraPay.charge_card(enc_data)
  {:ok, %{
      "amount" => 200,
      "amount_charged" => 200,
      "auth_model" => "OTP",
      "currency" => "NGN",
      "fee": 2.6,
      "vat": 0.2,
      "response_message" => "Please enter the OTP sent to your mobile number 080****** and email te**@rave**.com",
      "payment_reference"=> "1To64uTVkRcRA",
      "status" => "processing",
      "transaction_reference"=> "KPY-CA-b5Mit73shYeQ",
      "card" => {
        "card_type": "mastercard",
        "first_six": "539983",
        "last_four": "8381",
        "expiry": "10/31"
      }
    }
  }
  ```
  """
  @spec charge_card(String.t()) :: charge_response() | error()
  def charge_card(charge_data), do: impl().charge_card(charge_data)

  @doc """
  Payout to a bank account.

  ## Examples
  ```
  iex(1)> bank_account = %{"bank" => "033", "account" => "0000000000"}
  iex(2)> customer = %{"name" => "John Doe", "email" => "johndoe@korapay.com"}
  iex(3)> KoraPay.disburse(1000, "NGN", bank_account, customer)
  {:ok, %{
    "amount" => "100.00",
    "fee" => "2.50",
    "currency" => "NGN",
    "status" => "processing",
    "reference" => "KPY-D-t74azVrw9oPLtv9",
    "narration" => "Test Transfer Payment",
    "customer" => %{
      "name" => "John Doe",
      "email" => "johndoe@korapay.com"
      }
    }
  }
  ```

  ## Options
    - `reference` : transaction reference, if not provided, will be auto generated.
    - `type` : destination type. defaults to `"bank_account"`
  """
  @spec disburse(non_neg_integer(), String.t(), bank_account(), customer(), String.t()) ::
          disbursement() | error()
  def disburse(
        amount,
        currency,
        bank_account,
        customer,
        reference \\ generate_reference(),
        type \\ "bank_account"
      ) do
    params = %{
      reference: reference,
      destination: %{
        type: type,
        amount: to_string(amount),
        currency: currency,
        bank_account: bank_account,
        customer: customer
      }
    }

    impl().disburse(params)
  end

  @doc """
  Check on a payout's status.

  ## Examples
  ```
  iex(1)> KoraPay.verify_disbursement("KPY_jLo7Zbk")
  {:ok, %{
        "amount" => 1000,
        "fee" => 10,
        "narration" => "payout to customer",
        "currency" => "NGN",
        "created_at" => "2019-08-05T20:29:49.000Z",
        "status" => "success",
        "reference" => "KPY_jLo7Zbk",
        "callback_url" => "https://example.com",
        "trace_id" => "000000000000000111111111111111",
        "message" => "Payout successful",
        "customer" => %{
        "name" => "John Doe",
        "email" => "johndoe@korapay.com",
      }
    }
  }

  ```
  """
  @spec verify_disbursement(String.t()) :: disbursement_status() | error()
  def verify_disbursement(reference), do: impl().verify_disbursement(reference)

  @doc """
  All transactions for a client.

  ## Examples
  ```
  iex(1)> KoraPay.transactions()
  {:ok,  [
      %{
        "type" => "collection",
        "amount" => 1000,
        "fee" => 10,
        "narration" => "payment for a book",
        "currency" => "NGN",
        "created_at" => "2019-08-05T20:29:49.000Z",
        "status" => "success",
        "transaction_status" => "success",
        "reference" => "KPY-D-jLo7Zbk",
        "callback_url" => "https://example.com",
        "meta" => "{'product_name':'Becoming by Michelle Obama','product_description':''}",
        "customer" => %{
          "name" => "Timi Adesoji",
          "email" => "johndoe@korapay.com",
        }
      }, ...]}
  ```
  """
  @spec transactions() :: [transaction()] | error()
  def transactions, do: impl().transactions()

  @doc """
  Verify a bank account.

  ## Examples
  ```
  iex(1)> KoraPay.resolve_bank_account("058", "0234247896")
  {:ok, %{
   "account_name" => "OLAEGBE GBENGA EMMANUEL",
   "account_number" => "0234247896",
   "bank_code" => "058",
   "bank_name" => "GTBank Plc"
  }}
  ```
  """
  @spec resolve_bank_account(String.t(), String.t()) :: bank_account() | error()
  def resolve_bank_account(bank_code, account_number) do
    params = %{
      bank: bank_code,
      account: account_number
    }

    impl().resolve_bank_account(params)
  end

  @doc """
  All supported bank accounts.

  ## Example
  ```
  iex(1)> KoraPay.list_banks()
  {:ok, [%{
    name: "First Bank of Nigeria",
    slug: "firstbank",
    code: "011",
    nibss_bank_code: "000016",
    country: "NG"
    }, ...]}
    ```
  """
  @spec list_banks() :: [misc_bank_account()] | error()
  def list_banks, do: impl().list_banks()

  @doc """
  Return account balances. (currently naira only)

  ## Examples
  ```
  iex(1)> KoraPay.balances()
  {:ok, %{"NGN" => %{"available_balance" => 946, "pending_balance" => 0}}}
  ```
  """
  @spec balances :: balance() | error()
  def balances, do: impl().balances()

  @doc """
  Create a virutal bank account.

  ## Examples
  ```
  iex(1)> KoraPay.create_virtual_bank_account("Steph James", true, ["12345678901"], "035", %{ "name" => "Don Alpha"})
  ```
  ## Options
  1. `reference` : transaction reference, if not provided, will be auto generated.
  ##
  """
  @spec create_virtual_bank_account(
          String.t(),
          boolean(),
          [String.t()],
          String.t(),
          customer(),
          String.t()
        ) ::
          virtual_account() | error()
  def create_virtual_bank_account(
        name,
        permanent,
        bvn,
        bank_code,
        customer,
        reference \\ generate_reference()
      ) do
    params = %{
      account_name: name,
      account_reference: reference,
      permanent: permanent,
      bvn: bvn,
      bank_code: bank_code,
      customer: customer
    }

    impl().create_virtual_bank_account(params)
  end

  @spec virtual_bank_account_details(String.t()) :: virtual_account() | error()
  def virtual_bank_account_details(ref), do: impl().virtual_bank_account_details(ref)

  @spec virtual_bank_account_transactions(account_number :: String.t()) ::
          [virtual_bank_account_txn_response()] | error()
  def virtual_bank_account_transactions(account_number) do
    impl().virtual_bank_account_transactions(%{account_number: account_number})
  end

  defp generate_reference, do: UUID.uuid4(:slug)

  defp impl, do: Application.get_env(:kora_pay, :api, KoraPay.Client)
end