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