Skip to content

[Mercadopago] authorization tests #142

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
278 changes: 278 additions & 0 deletions lib/gringotts/gateways/mercadopago.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
defmodule Gringotts.Gateways.Mercadopago do
@moduledoc """
[mercadopago][home] gateway implementation.

For reference see [mercadopago documentation][docs].

The following features of mercadopago are implemented:

| Action | Method |
| ------ | ------ |
| Pre-authorize | `authorize/3` |
| Capture | `capture/3` |
| Purchase | `purchase/3` |
| Reversal | `void/2` |
| Refund | `refund/3` |

[home]: https://www.mercadopago.com/
[docs]: https://www.mercadopago.com.ar/developers/en/api-docs/

## 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 |
| `order_id` | Order id issued by the merchant. Type- integer |
| `customer_id` | Unique customer id issued by the gateway. For new customer it must skipped. Type- string|
| `order_type` | `"mercadopago"` or `"mercadolibre"` as per the order. Type- string |
| `installments` | No of installments for payment. Type- integer |

## Registering your mercadopago account at `Gringotts`

After [making an account on mercadopago][credentials], head to the credentials and find
your account "secrets" in the `Checkout Transparent`.

| Config parameter | MERCADOPAGO secret |
| ------- | ---- |
| `:access_token` | **Access Token** |
| `:public_key` | **Public Key** |

> Your Application config **must include the `[:public_key, :access_token]` field(s)** and would look
> something like this:
>
> config :gringotts, Gringotts.Gateways.Mercadopago,
> public_key: "your_secret_public_key"
> access_token: "your_secret_access_token"

[credentials]: https://www.mercadopago.com/mlb/account/credentials?type=basic

## Note

* mercadopago processes money in the subdivided units (like `cents` in case of
US Dollar).
* Also, there is no way to set the currency of the transaction via the API. It
is automatically set from the merchant's account. Hence, if you've
configured your mercadopago account to work in Chilean Peso (`CLP`), make
sure that the `amount` argument is always a `Money.t` struct with the `:CLP`
as currency.

## Supported currencies and countries

mercadopago supports the currencies listed [here][currencies].

[currencies]: https://api.mercadopago.com/currencies

## Following the examples

1. First, set up a sample application and configure it to work with MERCADOPAGO.
- 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-mercadopago-account-at-gringotts).

[gs]: https://github.com/aviabird/gringotts/wiki/
[home]: https://www.mercadopago.com
[example]: https://github.com/aviabird/gringotts_example
"""

@base_url "https://api.mercadopago.com"
use Gringotts.Gateways.Base
alias Gringotts.CreditCard
# 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: [:public_key, :access_token]

alias Gringotts.{CreditCard, 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.

mercadoapgo's `authorize` returns:
* `customer_id`, available in `Response.token` field and
* `authorization_id`, available in the `Response.id` field.

The `id` can be used to
* `capture/3` _an_ amount.
* `void/2` a pre-authorization.

## Note

For a new customer, `customer_id` field should be `nil`. Otherwise it should
be provided.

## Example

### Authorization for new customer.

The following example shows how one would (pre) authorize a payment of 42
BRL on a sample `card`.

iex> amount = Money.new(42, :BRL)
iex> card = %Gringotts.CreditCard{first_name: "Lord", last_name: "Voldemort", number: "4509953566233704", year: 2099, month: 12, verification_code: "123", brand: "VISA"}
iex> opts = [email: "[email protected]", order_id: 123123, payment_method_id: "visa"]
iex> {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.Mercadopago, amount, card, opts)
iex> auth_result.id # This is the authorization ID
iex> auth_result.token # This is the customer ID/token

### Authorization for old customer.

The following example shows how one would (pre) authorize a payment of 42
BRL on a sample `card`.

iex> amount = Money.new(42, :BRL)
iex> card = %Gringotts.CreditCard{first_name: "Hermione", last_name: "Granger", number: "4509953566233704", year: 2099, month: 12, verification_code: "123", brand: "VISA"}
iex> opts = [email: "[email protected]", order_id: 123125, customer_id: "hermoine's customer id", payment_method_id: "visa"]
iex> {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.Mercadopago, amount, card, opts)
iex> auth_result.id # This is the authorization ID
iex> auth_result.token # This is the customer ID/token
"""
@spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response}
def authorize(amount, %CreditCard{} = card, opts) do
with {:ok, customer_id} <- create_customer(opts),
{:ok, card_token} <- create_token(card, opts) do
{_, value, _, _} = Money.to_integer_exp(amount)
url_params = [access_token: opts[:config][:access_token]]

params = [
authorize_params(value, opts, card_token, false, card),
customer_params(card, customer_id, opts)
]

body =
params
|> Enum.reduce(&Map.merge/2)
|> Poison.encode!()

commit(:post, "/v1/payments", body, opts, params: url_params)
end
end

###############################################################################
# PRIVATE METHODS #
###############################################################################

# Makes the request to mercadopago'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(atom, String.t(), String.t(), keyword, keyword) :: {:ok | :error, Response.t()}
defp commit(method, path, body, opts, url_params) do
headers = [{"content-type", "application/json"}, {"accept", "application/json"}]
res = HTTPoison.request(method, "#{@base_url}#{path}", body, headers, url_params)
respond(res, opts)
end

# Parses mercadopago's response and returns a `Gringotts.Response` struct
# in a `:ok`, `:error` tuple.

defp create_customer(opts) do
if Keyword.has_key?(opts, :customer_id) do
{:ok, opts[:customer_id]}
else
url_params = [access_token: opts[:config][:access_token]]
body = %{email: opts[:email]} |> Poison.encode!()
{state, res} = commit(:post, "/v1/customers", body, opts, params: url_params)

if state == :error do
{state, res}
else
{state, res.id}
end
end
end

defp token_params(%CreditCard{} = card) do
%{
expirationYear: card.year,
expirationMonth: card.month,
cardNumber: card.number,
securityCode: card.verification_code,
cardholder: %{name: CreditCard.full_name(card)}
}
end

defp create_token(%CreditCard{} = card, opts) do
url_params = [public_key: opts[:config][:public_key]]
body = Poison.encode!(token_params(card))

{state, res} =
commit(:post, "/v1/card_tokens/#{opts[:customer_id]}", body, opts, params: url_params)

case state do
:error -> {state, res}
_ -> {state, res.id}
end
end

defp authorize_params(value, opts, token_id, capture, %CreditCard{} = card) do
%{
installments: opts[:installments] || 1,
transaction_amount: value,
payment_method_id: String.downcase(card.brand),
token: token_id,
capture: capture
}
end

defp customer_params(%CreditCard{} = card, customer_id, opts) do
%{
payer: %{
type: "customer",
id: customer_id,
first_name: card.first_name,
last_name: card.last_name
},
order: %{
type: opts[:order_type],
id: opts[:order_id]
}
}
end

defp success_body(body, status_code, opts) do
%Response{
success: true,
id: body["id"],
token: opts[:customer_id],
status_code: status_code,
message: body["status"]
}
end

defp error_body(body, status_code, opts) do
%Response{
success: false,
token: opts[:customer_id],
status_code: status_code,
message: body["message"]
}
end

defp respond({:ok, %HTTPoison.Response{body: body, status_code: status_code}}, opts) do
body = body |> Poison.decode!()

case body["cause"] 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
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ defmodule Gringotts.Mixfile do
{:mock, "~> 0.3.0", only: :test},
{:bypass, "~> 0.8", only: :test},
{:excoveralls, "~> 0.8", only: :test},
{:exvcr, "~> 0.10", only: :test},

# various analyses tools
{:credo, "~> 0.3", only: [:dev, :test]},
Expand Down
2 changes: 2 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
"ex_cldr_numbers": {:hex, :ex_cldr_numbers, "1.3.1", "50a117654dff8f8ee6958e68a65d0c2835a7e2f1aff94c1ea8f582c04fdf0bd4", [:mix], [{:decimal, "~> 1.4", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 1.4.0", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.1 or ~> 3.1", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
"ex_doc": {:hex, :ex_doc, "0.18.3", "f4b0e4a2ec6f333dccf761838a4b253d75e11f714b85ae271c9ae361367897b7", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"},
"ex_money": {:hex, :ex_money, "1.1.3", "843eed0a5673206de33be47cdc06574401abc3e2d33cbcf6d74e160226791ae4", [:mix], [{:decimal, "~> 1.4", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: true]}, {:ex_cldr, "~> 1.0", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 1.0", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}], "hexpm"},
"exactor": {:hex, :exactor, "2.2.4", "5efb4ddeb2c48d9a1d7c9b465a6fffdd82300eb9618ece5d34c3334d5d7245b1", [:mix], [], "hexpm"},
"excoveralls": {:hex, :excoveralls, "0.8.1", "0bbf67f22c7dbf7503981d21a5eef5db8bbc3cb86e70d3798e8c802c74fa5e27", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"},
"exvcr": {:hex, :exvcr, "0.10.0", "5150808404d9f48dbda636f70f7f8fefd93e2433cd39f695f810e73b3a9d1736", [:mix], [{:exactor, "~> 2.2", [hex: :exactor, repo: "hexpm", optional: false]}, {:exjsx, "~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:httpoison, "~> 0.13", [hex: :httpoison, repo: "hexpm", optional: true]}, {:httpotion, "~> 3.0", [hex: :httpotion, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"},
"gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm"},
"hackney": {:hex, :hackney, "1.11.0", "4951ee019df102492dabba66a09e305f61919a8a183a7860236c0fde586134b6", [:rebar3], [{:certifi, "2.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
"httpoison": {:hex, :httpoison, "0.13.0", "bfaf44d9f133a6599886720f3937a7699466d23bb0cd7a88b6ba011f53c6f562", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
Expand Down
87 changes: 0 additions & 87 deletions test/gringotts_test.exs

This file was deleted.

Loading