Current section
Files
Jump to
Current section
Files
lib/gringotts/gateways/openpay.ex
defmodule Gringotts.Gateways.Openpay do
@moduledoc """
[openpay][home] gateway implementation.
For reference see [openpay documentation][docs].
The following features of openpay are implemented:
| Action | Method |
| ------ | ------ |
| Pre-authorize | `authorize/3` |
| Capture | `capture/3` |
| Purchase | `purchase/3` |
| Reversal | `void/2` |
| Refund | `refund/3` |
[home]: https://www.openpay.mx/
[docs]: https://www.openpay.mx/en/docs/api/
## The `opts` argument
Most `Gringotts` API calls accept an optional `keyword` list `opts` to supply
optional arguments for transactions with the mercadopago
gateway. The following keys are supported:
| Key | Remark |
| ---- | --- |
| `email` | Email of the customer. Type - string |
| `device_session_id` | Identifier of the device generated by the antifraud tool. Type- string |
| `method` | It must contain the card value in order to specify the charge will be made from card. Type- string |
| `address` | Address of the card owner. Type object |
| `phone_number` | Customer phone number. Type- integer |
--------------------------------------------------------------------------------
## Registering your openpay account at `Gringotts`
After [making an account on openpay][dashboard], find
your account "secrets" in the `home page`
[dashboard]: https://sandbox-dashboard.openpay.mx/home.opay
> Here's how the secrets map to the required configuration parameters for openpay:
>
> | Config parameter | openpay secret |
> | ------- | ---- |
> | `:merchant_id` | **MerchantId** |
> | `:public_key` | **PublicKey** |
> Your Application config **must include the `[:merchant_id, :public_key]` field(s)** and would look
> something like this:
>
> config :gringotts, Gringotts.Gateways.Openpay,
> merchant_id: "your_secret_merchant_id"
> public_key: "your_secret_public_key"
## Note
mercadopago processes money with upto two decimal places.
## Supported currencies and countries
openpay support mexico country and :MXN currency.
## Following the examples
1. First, set up a sample application and configure it to work with openpay.
- You could do that from scratch by following our [Getting Started][gs] guide.
- To save you time, we recommend [cloning our example
repo][example] that gives you a pre-configured sample app ready-to-go.
+ You could use the same config or update it the with your "secrets"
as described [above](#module-registering-your-openpay-account-at-gringotts).
2. Run an `iex` session with `iex -S mix` and add some variable bindings and
aliases to it (to save some time):
[gs]: https://github.com/aviabird/gringotts/wiki/
[home]: https://www.openpay.com
[example]: https://github.com/aviabird/gringotts_example
"""
use Gringotts.Gateways.Base
# The Adapter module provides the `validate_config/1`
# Add the keys that must be present in the Application config in the
# `required_config` list
use Gringotts.Adapter, required_config: [:merchant_id, :public_key]
@base_url "https://sandbox-api.openpay.mx"
alias Gringotts.{CreditCard, Money, Response}
@doc """
Performs a (pre) Authorize operation.
The authorization validates the `card` details with the banking network,
places a hold on the transaction `amount` in the customer’s issuing bank and
also triggers risk management. Funds are not transferred.
openpay returns an ID string which can be used to:
* `capture/3` _an_ amount.
* `void/2` a pre-authorization.
## Example
The following example shows how one would (pre) authorize a payment of $42 on
a sample `card`.
iex> amount = Money.new(42, :MXN)
iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"}
iex> {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.Openpay, amount, card, opts)
iex> auth_result.id # This is the authorization ID
"""
@spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response}
def authorize(amount, %CreditCard{} = card, opts) do
with {:ok, token_id} <- create_token(card, opts) do
{_, value, _} = Money.to_integer(amount)
body =
card
|> auth_params(token_id, opts, value, false)
|> Jason.encode!()
commit(:post, "/v1/#{opts[:config][:merchant_id]}/charges", body, opts)
end
end
@doc """
Captures a pre-authorized `amount`.
`amount` is transferred to the merchant account by openpay when it is smaller or
equal to the amount used in the pre-authorization referenced by `payment_id`.
## Note
openpay allows partial captures and unlike many other gateways, does not release
the remaining amount back to the payment source. Thus, the same
pre-authorisation ID can be used to perform multiple captures, till:
* all the pre-authorized amount is captured or,
* the remaining amount is explicitly "reversed" via `void/2`.
## Example
The following example shows how one would (partially) capture a previously
authorized a payment worth $35 by referencing the obtained authorization `id`.
iex> amount = Money.new(35, :MXN)
iex> {:ok, capture_result} = Gringotts.capture(Gringotts.Gateways.Openpay, amount, auth_result.id, opts)
"""
@spec capture(String.t(), Money.t(), keyword) :: {:ok | :error, Response}
def capture(payment_id, amount, opts) do
{_, value, _} = Money.to_integer(amount)
body = %{amount: value} |> Jason.encode!()
commit(:post, "/v1/#{opts[:config][:merchant_id]}/charges/#{payment_id}/capture", body, opts)
end
@doc """
Transfers `amount` from the customer to the merchant.
openpay attempts to process a purchase on behalf of the customer, by debiting
`amount` from the customer's account by charging the customer's `card`.
## Example
The following example shows how one would process a payment worth $42 in
one-shot, without (pre) authorization.
iex> amount = Money.new(42, :MXN)
iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"}
iex> {:ok, purchase_result} = Gringotts.purchase(Gringotts.Gateways.Openpay, amount, card, opts)
"""
@spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response}
def purchase(amount, %CreditCard{} = card, opts) do
with {:ok, token_id} <- create_token(card, opts) do
{_, value, _} = Money.to_integer(amount)
body =
card
|> auth_params(token_id, opts, value, true)
|> Jason.encode!()
commit(:post, "/v1/#{opts[:config][:merchant_id]}/charges", body, opts)
end
end
@doc """
Voids the referenced payment.
This method attempts a reversal of a previous transaction referenced by
`payment_id`.
> As a consequence, the customer will never see any booking on his statement.
## Note
> Only pending or in_process payments can be cancelled.
> Cancelled coupon payments, deposits and/or transfers will be deposited in the buyer’s Openpay account.
## Example
The following example shows how one would void
iex> {:ok, void_result} = Gringotts.void(Gringotts.Gateways.Openpay, auth_result.id, opts)
"""
@spec void(String.t(), keyword) :: {:ok | :error, Response}
def void(payment_id, opts) do
body = %{} |> Jason.encode!()
commit(:post, "/v1/#{opts[:config][:merchant_id]}/charges/#{payment_id}/refund", body, opts)
end
@doc """
Refunds the `amount` to the customer's account with reference to a prior transfer.
openpay processes a full or partial refund worth `amount`, referencing a
previous `purchase/3` or `capture/3`.
## Example
The following example shows how one would (completely) refund a previous
purchase (and similarily for captures).
iex> amount = Money.new(42, :MXN)
iex> {:ok, refund_result} = Gringotts.refund(Gringotts.Gateways.Openpay, purchase_result.id, amount)
"""
@spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response}
def refund(amount, payment_id, opts) do
{_, value, _} = Money.to_integer(amount)
body = %{amount: value} |> Jason.encode!()
commit(:post, "/v1/#{opts[:config][:merchant_id]}/charges/#{payment_id}/refund", body, opts)
end
###############################################################################
# PRIVATE METHODS #
###############################################################################
# Makes the request to openpay's network.
# For consistency with other gateway implementations, make your (final)
# network request in here, and parse it using another private method called
# `respond`.
# @spec commit(_) :: {:ok | :error, Response}
defp commit(method, path, body, opts) do
headers = [
{"content-type", "application/json"},
{"Authorization", "Basic c2tfNjk1YjcxN2VhNmE1NGZlOTg2ZjRlNTJiZTJmZTdlNjE6"}
]
res = HTTPoison.request(method, "#{@base_url}#{path}", body, headers)
respond(res, opts)
end
# Parses openpay's response and returns a `Gringotts.Response` struct
# in a `:ok`, `:error` tuple.
# @spec respond(term) :: {:ok | :error, Response}
defp create_token(%CreditCard{} = card, opts) do
body =
card
|> token_params(opts)
|> Jason.encode!()
{state, res} = commit(:post, "/v1/#{opts[:config][:merchant_id]}/tokens", body, opts)
case state do
:ok -> {:ok, res.id}
:error -> {:error, res}
end
end
defp token_params(%CreditCard{} = card, opts) do
%{
card_number: card.number,
holder_name: CreditCard.full_name(card),
expiration_year: card.year,
expiration_month: card.month,
cvv2: card.verification_code |> String.to_integer(),
address: %{
city: opts[:address][:city],
country_code: opts[:address][:country_code],
postal_code: opts[:address][:postal_code],
line1: opts[:address][:line1],
line2: opts[:address][:line2],
line3: opts[:address][:line3],
state: opts[:address][:state]
}
}
end
defp auth_params(%CreditCard{} = card, token_id, opts, value, capture) do
%{
source_id: token_id,
method: opts[:method],
amount: value,
device_session_id: opts[:device_session_id],
capture: capture,
customer: %{
name: card.first_name,
last_name: card.last_name,
phone_number: opts[:phone_number],
email: opts[:email]
}
}
end
defp success_body(body, status_code, _opts) do
%Response{
success: true,
id: body["id"],
status_code: status_code,
message: body["status"]
}
end
defp error_body(body, status_code, _opts) do
%Response{
success: false,
status_code: status_code,
message: body["description"]
}
end
defp respond({:ok, %HTTPoison.Response{body: body, status_code: status_code}}, opts) do
body = body |> Jason.decode!()
case body["description"] do
nil -> {:ok, success_body(body, status_code, opts)}
_ -> {:error, error_body(body, status_code, opts)}
end
end
defp respond({:error, %HTTPoison.Error{} = error}, _) do
{
:error,
Response.error(
reason: "network related failure",
message: "HTTPoison says '#{error.reason}' [ID: #{error.id || "nil"}]"
)
}
end
end