diff --git a/lib/gringotts/gateways/mercadopago.ex b/lib/gringotts/gateways/mercadopago.ex new file mode 100644 index 00000000..a0469b08 --- /dev/null +++ b/lib/gringotts/gateways/mercadopago.ex @@ -0,0 +1,274 @@ +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 with upto two decimal places. + + ## 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). + + 2. Run an `iex` session with `iex -S mix` and add some variable bindings : + ``` + iex> card = %CreditCard{first_name: "John", last_name: "Doe", number: "4509953566233704", year: 2099, month: 12, verification_code: "123", brand: "VISA"} + ``` + + We'll be using these in the examples below. + + [gs]: https://github.com/aviabird/gringotts/wiki/ + [home]: https://www.mercadopago.com + [example]: https://github.com/aviabird/gringotts_example + """ + + # The Base module has the (abstract) public API, and some utility + # implementations. + @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`) and authorization id(available in the `Response.id` field) : + + * `capture/3` _an_ amount. + * `void/2` a pre-authorization. + ## Note + + For a new customer, `customer_id` field should be ignored. 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`. + Ignore `customer_id`. + 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: "tommarvolo@riddle.com", 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`. + Mention `customer_id`. + 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: "hermione@granger.com", 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 diff --git a/test/gringotts_test.exs b/test/gringotts_test.exs deleted file mode 100644 index 01d01824..00000000 --- a/test/gringotts_test.exs +++ /dev/null @@ -1,87 +0,0 @@ -defmodule GringottsTest do - use ExUnit.Case - - import Gringotts - - @test_config [ - some_auth_info: :merchant_secret_key, - other_secret: :sun_rises_in_the_east - ] - - @bad_config [some_auth_info: :merchant_secret_key] - - defmodule FakeGateway do - use Gringotts.Adapter, required_config: [:some_auth_info, :other_secret] - - def authorize(100, :card, _) do - :authorization_response - end - - def purchase(100, :card, _) do - :purchase_response - end - - def capture(1234, 100, _) do - :capture_response - end - - def void(1234, _) do - :void_response - end - - def refund(100, 1234, _) do - :refund_response - end - - def store(:card, _) do - :store_response - end - - def unstore(123, _) do - :unstore_response - end - end - - setup do - Application.put_env(:gringotts, GringottsTest.FakeGateway, @test_config) - :ok - end - - test "authorization" do - assert authorize(GringottsTest.FakeGateway, 100, :card, []) == :authorization_response - end - - test "purchase" do - assert purchase(GringottsTest.FakeGateway, 100, :card, []) == :purchase_response - end - - test "capture" do - assert capture(GringottsTest.FakeGateway, 1234, 100, []) == :capture_response - end - - test "void" do - assert void(GringottsTest.FakeGateway, 1234, []) == :void_response - end - - test "refund" do - assert refund(GringottsTest.FakeGateway, 100, 1234, []) == :refund_response - end - - test "store" do - assert store(GringottsTest.FakeGateway, :card, []) == :store_response - end - - test "unstore" do - assert unstore(GringottsTest.FakeGateway, 123, []) == :unstore_response - end - - test "validate_config when some required config is missing" do - Application.put_env(:gringotts, GringottsTest.FakeGateway, @bad_config) - - assert_raise( - ArgumentError, - "expected [:other_secret] to be set, got: [some_auth_info: :merchant_secret_key]\n", - fn -> authorize(GringottsTest.FakeGateway, 100, :card, []) end - ) - end -end