From d178069a7296005d86c55b94657b6e89e56fe780 Mon Sep 17 00:00:00 2001 From: Mandar Vaze Date: Mon, 30 Mar 2020 19:47:17 +0530 Subject: [PATCH 1/2] Add support for mercadopago gateway. Manually picked up code from various branches/PRs related to mercadopago The branches/PRs are couple years old, and messy. --- lib/gringotts/gateways/mercadopago.ex | 368 ++++++++++++++++++ mix.lock | 74 ++-- test/gateways/mercadopago_test.exs | 234 +++++++++++ .../integration/gateways/mercadopago_test.exs | 241 ++++++++++++ test/support/mocks/mercadopago_mock.ex | 226 +++++++++++ 5 files changed, 1106 insertions(+), 37 deletions(-) create mode 100644 lib/gringotts/gateways/mercadopago.ex create mode 100644 test/gateways/mercadopago_test.exs create mode 100644 test/integration/gateways/mercadopago_test.exs create mode 100644 test/support/mocks/mercadopago_mock.ex diff --git a/lib/gringotts/gateways/mercadopago.ex b/lib/gringotts/gateways/mercadopago.ex new file mode 100644 index 00000000..5945374e --- /dev/null +++ b/lib/gringotts/gateways/mercadopago.ex @@ -0,0 +1,368 @@ +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 mercadopag. 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_payment + """ + + @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, 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. + + 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: "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`. + + 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, card_token} <- create_token(card, opts) do + {currency, value, exponent} = Money.to_integer(amount) + url_params = [access_token: opts[:config][:access_token]] + + params = [ + authorize_params(value, opts, card_token, false, card) + ] + + body = + params + |> Enum.reduce(&Map.merge/2) + |> Poison.encode!() + + commit(:post, "/v1/payments", body, opts, url_params) + end + end + + @doc """ + Captures a pre-authorized `amount`. + `amount` is transferred to the merchant account by mercadopago used in the + pre-authorization referenced by `payment_id`. + ## Note + mercadopago allows partial captures also. However, you can make a partial capture to a payment only **once**. + > The authorization will be valid for 7 days. If you do not capture it by that time, it will be cancelled. + > The specified amount can not exceed the originally reserved. + > If you do not specify the amount, all the reserved money is captured. + > In Argentina only available for Visa and American Express cards. + ## Example + The following example shows how one would (partially) capture a previously + authorized a payment worth 35 BRL by referencing the obtained authorization `id`. + iex> amount = Money.new(35, :BRL) + iex> {:ok, capture_result} = Gringotts.capture(Gringotts.Gateways.Mercadopago, auth_result.id, amount, opts) + """ + @spec capture(String.t(), Money.t(), keyword) :: {:ok | :error, Response} + def capture(payment_id, amount, opts) do + {currency, value, exponent} = Money.to_integer(amount) + url_params = [access_token: opts[:config][:access_token]] + body = %{capture: true, transaction_amount: value} |> Poison.encode!() + commit(:put, "/v1/payments/#{payment_id}", body, opts, url_params) + end + + @doc """ + Transfers `amount` from the customer to the merchant. + mercadopago 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 BRL in + one-shot, without (pre) authorization. + iex> amount = Money.new(42, :BRL) + 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.Mercadopago, amount, card, opts) + iex> purchase_result.token # This is the customer ID/token + """ + @spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} + def purchase(amount, %CreditCard{} = card, opts) do + with {:ok, card_token} <- create_token(card, opts) do + {currency, value, exponent} = Money.to_integer(amount) + url_params = [access_token: opts[:config][:access_token]] + + body = value |> authorize_params(opts, card_token, true, card) |> Poison.encode!() + commit(:post, "/v1/payments", body, opts, url_params) + 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 Mercadopago account. + ## Example + The following example shows how one would void a previous (pre) + authorization. Remember that our `capture/3` example only did a partial + capture. + iex> {:ok, void_result} = Gringotts.void(Gringotts.Gateways.Mercadopago, auth_result.id, opts) + """ + @spec void(String.t(), keyword) :: {:ok | :error, Response} + def void(payment_id, opts) do + # url = "#{@base_url}/v1/payments/#{payment_id}?access_token=#{opts[:config][:access_token]}" + url_params = [access_token: opts[:config][:access_token]] + body = %{status: "cancelled"} |> Poison.encode!() + commit(:put, "/v1/payments/#{payment_id}", body, opts, url_params) + end + + @doc """ + Refunds the `amount` to the customer's account with reference to a prior transfer. + mercadopago processes a full or partial refund worth `amount`, referencing a + previous `purchase/3` or `capture/3`. + ## Note + > You must have enough available money in your account so you can refund the payment amount successfully. Otherwise, you'll get a 400 Bad Request error. + > You can refund a payment within 90 days after it was accredited. + > You can only refund approved payments. + > You can perform up to 20 partial refunds in one payment. + ## Example + The following example shows how one would (completely) refund a previous + purchase (and similarily for captures). + iex> amount = Money.new(35, :BRL) + iex> {:ok, refund_result} = Gringotts.refund(Gringotts.Gateways.Mercadopago, purchase_result.id, amount) + """ + @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response} + def refund(amount, payment_id, opts) do + {currency, value, exponent} = Money.to_integer(amount) + + # url = + # "#{@base_url}/v1/payments/#{payment_id}/refunds?access_token=#{opts[:config][:access_token]}" + url_params = [access_token: opts[:config][:access_token]] + body = %{amount: value} |> Poison.encode!() + commit(:post, "/v1/payments/#{payment_id}/refunds", body, opts, url_params) + 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"}] + url = "#{@base_url}#{path}" + + res = + HTTPoison.request( + method, + url, + body, + headers, + opts ++ [params: [access_token: opts[:config][:access_token]]] + ) + + respond(res, opts) + 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 = + card + |> token_params() + |> Poison.encode!() + + {state, res} = commit(:post, "/v1/card_tokens/#{opts[:customer_id]}", body, opts, 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, + payer: %{ + email: opts[:email] + } + } + 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/mix.lock b/mix.lock index 704128b0..bd96dbe6 100644 --- a/mix.lock +++ b/mix.lock @@ -1,43 +1,43 @@ %{ "abnf2": {:hex, :abnf2, "0.1.2", "6f8792b8ac3288dba5fc889c2bceae9fe78f74e1a7b36bea9726ffaa9d7bef95", [:mix], [], "hexpm"}, - "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, - "bypass": {:hex, :bypass, "0.8.1", "16d409e05530ece4a72fabcf021a3e5c7e15dcc77f911423196a0c551f2a15ca", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, - "certifi": {:hex, :certifi, "2.3.1", "d0f424232390bf47d82da8478022301c561cf6445b5b5fb6a84d49a9e76d2639", [:rebar3], [{:parse_trans, "3.2.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, - "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"}, - "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, - "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"}, - "credo": {:hex, :credo, "0.9.3", "76fa3e9e497ab282e0cf64b98a624aa11da702854c52c82db1bf24e54ab7c97a", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, - "decimal": {:hex, :decimal, "1.5.0", "b0433a36d0e2430e3d50291b1c65f53c37d56f83665b43d79963684865beab68", [:mix], [], "hexpm"}, - "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"}, - "earmark": {:hex, :earmark, "1.2.5", "4d21980d5d2862a2e13ec3c49ad9ad783ffc7ca5769cf6ff891a4553fbaae761", [:mix], [], "hexpm"}, - "elixir_xml_to_map": {:hex, :elixir_xml_to_map, "0.1.2", "e3d1bd2f6562711117ae209657f385a1c1c34c8c720c748eeba2e22815797071", [:mix], [{:erlsom, "~>1.4", [hex: :erlsom, repo: "hexpm", optional: false]}], "hexpm"}, - "erlsom": {:hex, :erlsom, "1.4.2", "5cddb82fb512f406f61162e511ae86582f824f0dccda788378b18a00d89c1b3f", [:rebar3], [], "hexpm"}, + "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, + "bypass": {:hex, :bypass, "0.8.1", "16d409e05530ece4a72fabcf021a3e5c7e15dcc77f911423196a0c551f2a15ca", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "61fc89e67e448785905d35b2b06a3a36ae0cf0857c343fd65c753af42406f31a"}, + "certifi": {:hex, :certifi, "2.3.1", "d0f424232390bf47d82da8478022301c561cf6445b5b5fb6a84d49a9e76d2639", [:rebar3], [{:parse_trans, "3.2.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "e12d667d042c11d130594bae2b0097e63836fe8b1e6d6b2cc48f8bb7a2cf7d68"}, + "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, + "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "f4763bbe08233eceed6f24bc4fcc8d71c17cfeafa6439157c57349aa1bb4f17c"}, + "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm", "db622da03aa039e6366ab953e31186cc8190d32905e33788a1acb22744e6abd2"}, + "credo": {:hex, :credo, "0.9.3", "76fa3e9e497ab282e0cf64b98a624aa11da702854c52c82db1bf24e54ab7c97a", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "dcd1d45626f6a02abeef3fc424eaf101b05a851d3cceb9535b8ea3e14c3c17e6"}, + "decimal": {:hex, :decimal, "1.5.0", "b0433a36d0e2430e3d50291b1c65f53c37d56f83665b43d79963684865beab68", [:mix], [], "hexpm", "130926580655f34d759dd25f5d723fd233c9bbe0399cde57e2a1adea9ed92e08"}, + "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm", "6c32a70ed5d452c6650916555b1f96c79af5fc4bf286997f8b15f213de786f73"}, + "earmark": {:hex, :earmark, "1.2.5", "4d21980d5d2862a2e13ec3c49ad9ad783ffc7ca5769cf6ff891a4553fbaae761", [:mix], [], "hexpm", "c57508ddad47dfb8038ca6de1e616e66e9b87313220ac5d9817bc4a4dc2257b9"}, + "elixir_xml_to_map": {:hex, :elixir_xml_to_map, "0.1.2", "e3d1bd2f6562711117ae209657f385a1c1c34c8c720c748eeba2e22815797071", [:mix], [{:erlsom, "~>1.4", [hex: :erlsom, repo: "hexpm", optional: false]}], "hexpm", "a134d24496ebb25e1ab7027bba18a3be1f91f44aa3e6701bdc6ea5807d98ef0a"}, + "erlsom": {:hex, :erlsom, "1.4.2", "5cddb82fb512f406f61162e511ae86582f824f0dccda788378b18a00d89c1b3f", [:rebar3], [], "hexpm", "ac989e850a5a4c1641694f77506804710315f3d1193c977a36b223a32859edd3"}, "ex_cldr": {:hex, :ex_cldr, "1.4.4", "654966e8724d607e5cf9ecd5509ffcf66868b17e479bbd22ab2e9123595f9103", [:mix], [{:abnf2, "~> 0.1", [hex: :abnf2, repo: "hexpm", optional: false]}, {:decimal, "~> 1.4", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.13", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}, {:poison, "~> 2.1 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm"}, "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_doc": {:hex, :ex_doc, "0.18.3", "f4b0e4a2ec6f333dccf761838a4b253d75e11f714b85ae271c9ae361367897b7", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm", "33d7b70d87d45ed899180fb0413fb77c7c48843188516e15747e00fdecf572b6"}, "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.2", "b941a08a1842d7aa629e0bbc969186a4cefdd035bad9fe15d43aaaaaeb8fae36", [: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.2", "a66a0fa86d03153e5c21e38b1320d10b537038d7bc7b10dcc1ab7f0343569822", [:mix], [{:exactor, "~> 2.2", [hex: :exactor, repo: "hexpm", optional: false]}, {:exjsx, "~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: true]}, {:httpotion, "~> 3.1", [hex: :httpotion, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:meck, "~> 0.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"}, - "gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm"}, - "hackney": {:hex, :hackney, "1.12.1", "8bf2d0e11e722e533903fe126e14d6e7e94d9b7983ced595b75f532e04b7fdc7", [:rebar3], [{:certifi, "2.3.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.1", [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, "1.1.1", "96ed7ab79f78a31081bb523eefec205fd2900a02cda6dbc2300e7a1226219566", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, - "idna": {:hex, :idna, "5.1.1", "cbc3b2fa1645113267cc59c760bafa64b2ea0334635ef06dbac8801e42f7279c", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, - "inch_ex": {:hex, :inch_ex, "0.5.6", "418357418a553baa6d04eccd1b44171936817db61f4c0840112b420b8e378e67", [:mix], [{:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, - "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm"}, - "meck": {:hex, :meck, "0.8.9", "64c5c0bd8bcca3a180b44196265c8ed7594e16bcc845d0698ec6b4e577f48188", [:rebar3], [], "hexpm"}, - "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, - "mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm"}, - "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, - "mock": {:hex, :mock, "0.3.1", "994f00150f79a0ea50dc9d86134cd9ebd0d177ad60bd04d1e46336cdfdb98ff9", [:mix], [{:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"}, - "parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm"}, - "plug": {:hex, :plug, "1.5.1", "1ff35bdecfb616f1a2b1c935ab5e4c47303f866cb929d2a76f0541e553a58165", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.3", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"}, - "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, - "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}, - "timex": {:hex, :timex, "3.3.0", "e0695aa0ddb37d460d93a2db34d332c2c95a40c27edf22fbfea22eb8910a9c8d", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, - "tzdata": {:hex, :tzdata, "0.5.16", "13424d3afc76c68ff607f2df966c0ab4f3258859bbe3c979c9ed1606135e7352", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"}, - "xml_builder": {:hex, :xml_builder, "2.1.0", "c249d5339427c13cae11e9d9d0e8b40d25d228b9ecc54029f24017385e60280b", [:mix], [], "hexpm"}, + "exactor": {:hex, :exactor, "2.2.4", "5efb4ddeb2c48d9a1d7c9b465a6fffdd82300eb9618ece5d34c3334d5d7245b1", [:mix], [], "hexpm", "1222419f706e01bfa1095aec9acf6421367dcfab798a6f67c54cf784733cd6b5"}, + "excoveralls": {:hex, :excoveralls, "0.8.2", "b941a08a1842d7aa629e0bbc969186a4cefdd035bad9fe15d43aaaaaeb8fae36", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "d88423ffec403e84d0b74c34ef952e7d6b029d3191f5926355c7af3adab9888f"}, + "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm", "32e95820a97cffea67830e91514a2ad53b888850442d6d395f53a1ac60c82e07"}, + "exvcr": {:hex, :exvcr, "0.10.2", "a66a0fa86d03153e5c21e38b1320d10b537038d7bc7b10dcc1ab7f0343569822", [:mix], [{:exactor, "~> 2.2", [hex: :exactor, repo: "hexpm", optional: false]}, {:exjsx, "~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: true]}, {:httpotion, "~> 3.1", [hex: :httpotion, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:meck, "~> 0.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "b8f300020fac255a9062cf6291f11c143b5e23b3548c399ef7b16ebe8fe4e5bc"}, + "gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm", "d17ac3aa40835f7feb92913fcdf655edea59938fa8b25f5ce5d423e76a695c3b"}, + "hackney": {:hex, :hackney, "1.12.1", "8bf2d0e11e722e533903fe126e14d6e7e94d9b7983ced595b75f532e04b7fdc7", [:rebar3], [{:certifi, "2.3.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.1", [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", "8d5c94391a1dd525e58713b4fb43be9a930360ea8e74d0474e535ff579df6071"}, + "httpoison": {:hex, :httpoison, "1.1.1", "96ed7ab79f78a31081bb523eefec205fd2900a02cda6dbc2300e7a1226219566", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "d480e9b89a1da274393c0a17cd548581ce7b2c45ac9d5ae2cdda8fd66d906295"}, + "idna": {:hex, :idna, "5.1.1", "cbc3b2fa1645113267cc59c760bafa64b2ea0334635ef06dbac8801e42f7279c", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "12197a282ab74a30dbe5853ec4d1dca3332f1fdc8ebed682c083e467d64f6491"}, + "inch_ex": {:hex, :inch_ex, "0.5.6", "418357418a553baa6d04eccd1b44171936817db61f4c0840112b420b8e378e67", [:mix], [{:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "7123ca0450686a61416a06cd38e26af18fd0f8c1cff5214770a957c6e0724338"}, + "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm", "fc3499fed7a726995aa659143a248534adc754ebd16ccd437cd93b649a95091f"}, + "meck": {:hex, :meck, "0.8.9", "64c5c0bd8bcca3a180b44196265c8ed7594e16bcc845d0698ec6b4e577f48188", [:rebar3], [], "hexpm", "5eb607516f4a644324f130d2ad8893d4097020e8d6097193d9f7be55ee8d00d6"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, + "mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm", "5e839994289d60326aa86020c4fbd9c6938af188ecddab2579f07b66cd665328"}, + "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm", "7a4c8e1115a2732a67d7624e28cf6c9f30c66711a9e92928e745c255887ba465"}, + "mock": {:hex, :mock, "0.3.1", "994f00150f79a0ea50dc9d86134cd9ebd0d177ad60bd04d1e46336cdfdb98ff9", [:mix], [{:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "dda3bf758218e7c6179c658b7a12f01271e68b2551c1bfdd70b8ea2178ef8a6e"}, + "parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm", "578b1d484720749499db5654091ddac818ea0b6d568f2c99c562d2a6dd4aa117"}, + "plug": {:hex, :plug, "1.5.1", "1ff35bdecfb616f1a2b1c935ab5e4c47303f866cb929d2a76f0541e553a58165", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.3", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "aa312d5df0f815eed879aaa77543534e21c78ca4a7e983083698fc190796fd9c"}, + "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"}, + "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm", "6e56493a862433fccc3aca3025c946d6720d8eedf6e3e6fb911952a7071c357f"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm", "4f8805eb5c8a939cf2359367cb651a3180b27dfb48444846be2613d79355d65e"}, + "timex": {:hex, :timex, "3.3.0", "e0695aa0ddb37d460d93a2db34d332c2c95a40c27edf22fbfea22eb8910a9c8d", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "87a1644b84d9f9db438e194ce23a59c8f7e0198ec9e8c33eaa7a34d543896be1"}, + "tzdata": {:hex, :tzdata, "0.5.16", "13424d3afc76c68ff607f2df966c0ab4f3258859bbe3c979c9ed1606135e7352", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "fe63d21f1cf5d85303617a0585f8176bcb1b68cec46f0623b8a9dedff778579b"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm", "da1d9bef8a092cc7e1e51f1298037a5ddfb0f657fe862dfe7ba4c5807b551c29"}, + "xml_builder": {:hex, :xml_builder, "2.1.0", "c249d5339427c13cae11e9d9d0e8b40d25d228b9ecc54029f24017385e60280b", [:mix], [], "hexpm", "7d0e4fbc4fe5eddc6f1ff88dcfeeca8bcbf49c49809d6280db7b90d714c04c75"}, } diff --git a/test/gateways/mercadopago_test.exs b/test/gateways/mercadopago_test.exs new file mode 100644 index 00000000..3226712f --- /dev/null +++ b/test/gateways/mercadopago_test.exs @@ -0,0 +1,234 @@ +defmodule Gringotts.Gateways.MercadopagoTest do + use ExUnit.Case, async: false + alias Gringotts.Gateways.MercadopagoMock, as: MockResponse + alias Gringotts.{CreditCard, FakeMoney} + alias Gringotts.Gateways.Mercadopago, as: Merc + + import Mock + + @auth %{name: "64jKa6NA", transaction_key: "4vmE338dQmAN6m7B"} + @card %CreditCard{ + number: "5424000000000015", + month: 12, + year: 2099, + verification_code: 999, + brand: "visa" + } + + @bad_card %CreditCard{ + number: "123", + month: 10, + year: 2010, + verification_code: 123, + brand: "visa" + } + + @amount FakeMoney.new("2.99", :USD) + + @opts [ + config: @auth, + ref_id: "123456", + order: %{invoice_number: "INV-12345", description: "Product Description"}, + lineitems: %{ + item_id: "1", + name: "vase", + description: "Cannes logo", + quantity: 18, + unit_price: FakeMoney.new("53.82", :USD) + }, + tax: %{name: "VAT", amount: FakeMoney.new("0.1", :EUR), description: "Value Added Tax"}, + shipping: %{ + name: "SAME-DAY-DELIVERY", + amount: FakeMoney.new("0.56", :EUR), + description: "Zen Logistics" + }, + duty: %{ + name: "import_duty", + amount: FakeMoney.new("0.25", :EUR), + description: "Upon import of goods" + } + ] + @opts_refund [ + config: @auth, + ref_id: "123456", + payment: %{card: %{number: "5424000000000015", year: 2099, month: 12}} + ] + + @opts_store [ + config: @auth, + profile: %{ + merchant_customer_id: "123456", + description: "Profile description here", + email: "customer-profile-email@here.com" + }, + customer_type: "individual", + validation_mode: "testMode" + ] + @opts_store_without_validation [ + config: @auth, + profile: %{ + merchant_customer_id: "123456", + description: "Profile description here", + email: "customer-profile-email@here.com" + } + ] + + @opts_store_no_profile [ + config: @auth + ] + @opts_refund [ + config: @auth, + ref_id: "123456", + payment: %{card: %{number: "5424000000000015", year: 2099, month: 12}} + ] + @opts_refund_bad_payment [ + config: @auth, + ref_id: "123456", + payment: %{card: %{number: "123", year: 2099, month: 12}} + ] + @opts_store [ + config: @auth, + profile: %{ + merchant_customer_id: "123456", + description: "Profile description here", + email: "customer-profile-email@here.com" + } + ] + @opts_store_no_profile [ + config: @auth + ] + @opts_customer_profile [ + config: @auth, + customer_profile_id: "1814012002", + validation_mode: "testMode", + customer_type: "individual" + ] + @opts_customer_profile_args [ + config: @auth, + customer_profile_id: "1814012002" + ] + + @refund_id "60036752756" + @void_id "60036855217" + @void_invalid_id "60036855211" + @unstore_id "1813991490" + @capture_id "60036752756" + @capture_invalid_id "60036855211" + + @refund_id "60036752756" + @void_id "60036855217" + @unstore_id "1813991490" + + describe "purchase" do + test "successful response with right params" do + with_mock HTTPoison, + request: fn _method, _url, _body, _headers, _options -> + MockResponse.successful_purchase_response() + end do + assert {:ok, _response} = Merc.purchase(@amount, @card, @opts) + end + end + + test "with bad card" do + with_mock HTTPoison, + request: fn _method, _url, _body, _headers, _options -> + MockResponse.bad_card_purchase_response() + end do + assert {:error, _response} = Merc.purchase(@amount, @bad_card, @opts) + end + end + end + + describe "authorize" do + test "successful response with right params" do + with_mock HTTPoison, + request: fn _method, _url, _body, _headers, _options -> + MockResponse.successful_authorize_response() + end do + assert {:ok, _response} = Merc.authorize(@amount, @card, @opts) + end + end + + test "with bad card" do + with_mock HTTPoison, + request: fn _method, _url, _body, _headers, _options -> + MockResponse.bad_card_purchase_response() + end do + assert {:error, _response} = Merc.authorize(@amount, @bad_card, @opts) + end + end + end + + describe "capture" do + test "successful response with right params" do + with_mock HTTPoison, + request: fn _method, _url, _body, _headers, _options -> + MockResponse.successful_capture_response() + end do + assert {:ok, _response} = Merc.capture(@capture_id, @amount, @opts) + end + end + + test "with bad transaction id" do + with_mock HTTPoison, + request: fn _method, _url, _body, _headers, _options -> MockResponse.bad_id_capture() end do + assert {:error, _response} = Merc.capture(@capture_invalid_id, @amount, @opts) + end + end + end + + describe "refund" do + test "successful response with right params" do + with_mock HTTPoison, + request: fn _method, _url, _body, _headers, _options -> + MockResponse.successful_refund_response() + end do + assert {:ok, _response} = Merc.refund(@amount, @refund_id, @opts_refund) + end + end + + test "bad payment params" do + with_mock HTTPoison, + request: fn _method, _url, _body, _headers, _options -> MockResponse.bad_card_refund() end do + assert {:error, _response} = Merc.refund(@amount, @refund_id, @opts_refund_bad_payment) + end + end + + test "debit less than refund amount" do + with_mock HTTPoison, + request: fn _method, _url, _body, _headers, _options -> + MockResponse.debit_less_than_refund() + end do + assert {:error, _response} = Merc.refund(@amount, @refund_id, @opts_refund) + end + end + end + + describe "void" do + test "successful response with right params" do + with_mock HTTPoison, + request: fn _method, _url, _body, _headers, _options -> MockResponse.successful_void() end do + assert {:ok, _response} = Merc.void(@void_id, @opts) + end + end + + test "with bad transaction id" do + with_mock HTTPoison, + request: fn _method, _url, _body, _headers, _options -> + MockResponse.void_non_existent_id() + end do + assert {:error, _response} = Merc.void(@void_invalid_id, @opts) + end + end + end + + test "network error type non existent domain" do + with_mock HTTPoison, + request: fn _method, _url, _body, _headers, _options -> + MockResponse.netwok_error_non_existent_domain() + end do + assert {:error, response} = Merc.purchase(@amount, @card, @opts) + assert response.message == "HTTPoison says 'nxdomain' [ID: nil]" + end + end +end diff --git a/test/integration/gateways/mercadopago_test.exs b/test/integration/gateways/mercadopago_test.exs new file mode 100644 index 00000000..95da249a --- /dev/null +++ b/test/integration/gateways/mercadopago_test.exs @@ -0,0 +1,241 @@ +defmodule Gringotts.Integration.Gateways.MercadopagoTest do + # Integration tests for the Mercadopago + + use ExUnit.Case, async: true + alias Gringotts.Gateways.Mercadopago, as: Gateway + + alias Gringotts.{ + CreditCard, + FakeMoney + } + + @moduletag integration: true + + @amount FakeMoney.new(45, :BRL) + @sub_amount FakeMoney.new(30, :BRL) + @config [ + access_token: "TEST-4543588471539213-040810-f4f850f89480ee1bd56e05a9aa0d6210-543713181", + public_key: "TEST-4508ea76-c56b-436a-9213-57934dfb2d86" + ] + @bad_config [ + access_token: "TEST-4543588471539213-111111-f4f850f89480ee1bd56e05a9aa0d6210-543713181", + public_key: "TEST-4508ea76-c56b-436a-9999-57934dfb2d86" + ] + @good_card %CreditCard{ + first_name: "Hermoine", + last_name: "Grangerr", + number: "4509953566233704", + year: 2030, + month: 07, + verification_code: "123", + brand: "VISA" + } + + @bad_card %CreditCard{ + first_name: "Hermoine", + last_name: "Grangerr", + number: "4509953566233704", + year: 2000, + month: 07, + verification_code: "123", + brand: "VISA" + } + + @good_opts [ + email: "hermoine@granger.com", + order_id: 123_126, + customer_id: "311211654-YrXF6J0QikpIWX", + config: @config, + installments: 1, + order_type: "mercadopago" + ] + @bad_opts [ + email: "hermoine@granger.com", + config: @bad_config + ] + + @new_cutomer_good_opts [ + order_id: 123_126, + config: @config, + installments: 1, + order_type: "mercadopago" + ] + @new_cutomer_bad_opts [ + config: @bad_config, + order_id: 123_127 + ] + + def new_email_opts(good) do + no1 = 100_000 |> :rand.uniform() |> to_string + no2 = 100_000 |> :rand.uniform() |> to_string + no3 = 100_000 |> :rand.uniform() |> to_string + email = "hp" <> no1 <> no2 <> no3 <> "@potter.com" + + case good do + true -> @new_cutomer_good_opts ++ [email: email] + _ -> @new_cutomer_bad_opts ++ [email: email] + end + end + + describe "[authorize] old customer" do + test "old customer with good_opts and good_card" do + assert {:ok, response} = Gateway.authorize(@amount, @good_card, @good_opts) + assert response.success == true + assert response.status_code == 201 + end + + test "old customer with good_opts and bad_card" do + assert {:error, response} = Gateway.authorize(@amount, @bad_card, @good_opts) + assert response.success == false + assert response.status_code == 400 + end + end + + setup do + [opts: new_email_opts(true)] + end + + describe "[authorize] new customer" do + test "new cutomer with good_opts and good_card", %{opts: opts} do + assert {:ok, response} = Gateway.authorize(@amount, @good_card, opts) + assert response.success == true + assert response.status_code == 201 + end + + test "new customer with good_opts and bad_card", %{opts: opts} do + assert {:error, response} = Gateway.authorize(@amount, @bad_card, opts) + assert response.success == false + assert response.status_code == 400 + end + end + + describe "[capture]" do + test "capture success" do + {:ok, response} = Gateway.authorize(@sub_amount, @good_card, @good_opts) + {:ok, response} = Gateway.capture(response.id, @sub_amount, @good_opts) + assert response.success == true + assert response.status_code == 200 + end + + test "capture invalid payment_id" do + {:ok, response} = Gateway.authorize(@sub_amount, @good_card, @good_opts) + id = response.id + 1 + {:error, response} = Gateway.capture(id, @sub_amount, @good_opts) + assert response.success == false + assert response.status_code == 404 + end + + test "extra amount capture" do + {:ok, response} = Gateway.authorize(@sub_amount, @good_card, @good_opts) + {:error, response} = Gateway.capture(response.id, @amount, @good_opts) + assert response.success == false + assert response.status_code == 400 + end + end + + describe "[void]" do + test "void success" do + {:ok, response} = Gateway.authorize(@sub_amount, @good_card, @good_opts) + {:ok, response} = Gateway.void(response.id, @good_opts) + assert response.success == true + assert response.status_code == 200 + end + + test "invalid payment_id" do + {:ok, response} = Gateway.authorize(@sub_amount, @good_card, @good_opts) + id = response.id + 1 + {:error, response} = Gateway.void(id, @good_opts) + assert response.success == false + assert response.status_code == 404 + end + end + + describe "[purchase]" do + test "old customer with good_opts and good_card" do + assert {:ok, response} = Gateway.purchase(@amount, @good_card, @good_opts) + assert response.success == true + assert response.status_code == 201 + end + + test "old customer with good_opts and bad_card" do + assert {:error, response} = Gateway.purchase(@amount, @bad_card, @good_opts) + assert response.success == false + assert response.status_code == 400 + end + + test "old customer with bad_opts and good_card" do + assert {:error, response} = Gateway.purchase(@amount, @good_card, @bad_opts) + assert response.success == false + # We expect 401-Unauthorized when bad access_token is provided. + # But mergadopago API returns 404 with message "invalid token" instead. + # So that is what we check + assert response.status_code == 404 + assert response.message == "invalid_token" + end + + test "old customer with bad_opts and bad_card" do + assert {:error, response} = Gateway.purchase(@amount, @bad_card, @bad_opts) + assert response.success == false + assert response.status_code == 400 + assert response.message == "invalid expiration_year" + end + + test "new cutomer with good_opts and good_card" do + opts = new_email_opts(true) + assert {:ok, response} = Gateway.purchase(@amount, @good_card, opts) + assert response.success == true + assert response.status_code == 201 + end + + test "new customer with good_opts and bad_card" do + opts = new_email_opts(true) + assert {:error, response} = Gateway.purchase(@amount, @bad_card, opts) + assert response.success == false + assert response.status_code == 400 + assert response.message == "invalid expiration_year" + end + + test "new customer with bad_opts and good_card" do + opts = new_email_opts(false) + assert {:error, response} = Gateway.purchase(@amount, @good_card, opts) + assert response.success == false + # We expect 401-Unauthorized when bad access_token is provided. + # But mergadopago API returns 404 with message "invalid token" instead. + # So that is what we check + assert response.status_code == 404 + assert response.message == "invalid_token" + end + + test "new customer with bad_opts and bad_card" do + opts = new_email_opts(false) + assert {:error, response} = Gateway.purchase(@amount, @bad_card, opts) + assert response.success == false + assert response.status_code == 400 + assert response.message == "invalid expiration_year" + end + end + + describe "[refund]" do + test "refund success" do + {:ok, response} = Gateway.purchase(@sub_amount, @good_card, @good_opts) + {:ok, response} = Gateway.refund(@sub_amount, response.id, @good_opts) + assert response.success == true + assert response.status_code == 201 + end + + test "invalid payment_id" do + {:ok, response} = Gateway.purchase(@sub_amount, @good_card, @good_opts) + id = response.id + 1 + {:error, response} = Gateway.refund(@sub_amount, id, @good_opts) + assert response.success == false + assert response.status_code == 404 + end + + test "extra amount refund" do + {:ok, response} = Gateway.purchase(@sub_amount, @good_card, @good_opts) + {:error, response} = Gateway.refund(@amount, response.id, @good_opts) + assert response.success == false + assert response.status_code == 400 + end + end +end diff --git a/test/support/mocks/mercadopago_mock.ex b/test/support/mocks/mercadopago_mock.ex new file mode 100644 index 00000000..f1c18779 --- /dev/null +++ b/test/support/mocks/mercadopago_mock.ex @@ -0,0 +1,226 @@ +defmodule Gringotts.Gateways.MercadopagoMock do + @base_url "https://api.mercadopago.com" + + @moduledoc false + # purchase mock response + def successful_purchase_response do + {:ok, + %HTTPoison.Response{ + # body: {"id": 20359978, "date_created": "2019-07-10T10:47:58.000-04:00", "date_approved": "2019-07-10T10:47:58.000-04:00", "date_last_updated": "2019-07-10T10:47:58.000-04:00", "date_of_expiration": null, "money_release_date": "2019-07-24T10:47:58.000-04:00", "operation_type": "regular_payment", "issuer_id": "25", "payment_method_id": "visa", "payment_type_id": "credit_card", "status": "approved"}, + body: + "{\"id\":24687003,\"date_created\":\"2019-07-10T03:23:38.000-04:00\",\"date_approved\":\"2019-07-10T03:23:38.000-04:00\",\"date_last_updated\":\"2019-07-10T03:23:38.000-04:00\",\"date_of_expiration\":null,\"money_release_date\":\"2020-04-14T03:23:38.000-04:00\",\"operation_type\":\"regular_payment\",\"issuer_id\":\"25\",\"payment_method_id\":\"visa\",\"payment_type_id\":\"credit_card\",\"status\":\"approved\",\"status_detail\":\"accredited\",\"currency_id\":\"BRL\",\"description\":null,\"live_mode\":false,\"sponsor_id\":null,\"authorization_code\":null,\"money_release_schema\":null,\"taxes_amount\":0,\"counter_currency\":null,\"shipping_amount\":0,\"pos_id\":null,\"store_id\":null,\"integrator_id\":null,\"platform_id\":null,\"corporation_id\":null,\"collector_id\":543713181,\"payer\":{\"first_name\":\"Test\",\"last_name\":\"Test\",\"email\":\"test_user_80507629@testuser.com\",\"identification\":{\"number\":\"32659430\",\"type\":\"DNI\"},\"phone\":{\"area_code\":\"01\",\"number\":\"1111-1111\",\"extension\":\"\"},\"type\":\"registered\",\"entity_type\":null,\"id\":\"546439110\"},\"marketplace_owner\":null,\"metadata\":{},\"additional_info\":{\"available_balance\":null,\"nsu_processadora\":null},\"order\":{},\"external_reference\":null,\"transaction_amount\":4500,\"transaction_amount_refunded\":0,\"coupon_amount\":0,\"differential_pricing_id\":null,\"deduction_schema\":null,\"installments\":1,\"transaction_details\":{\"payment_method_reference_id\":null,\"net_received_amount\":4275.45,\"total_paid_amount\":4500,\"overpaid_amount\":0,\"external_resource_url\":null,\"installment_amount\":4500,\"financial_institution\":null,\"payable_deferral_period\":null,\"acquirer_reference\":null},\"fee_details\":[{\"type\":\"mercadopago_fee\",\"amount\":224.55,\"fee_payer\":\"collector\"}],\"captured\":true,\"binary_mode\":false,\"call_for_authorize_id\":null,\"statement_descriptor\":\"ASHISHSINGH\",\"card\":{\"id\":null,\"first_six_digits\":\"450995\",\"last_four_digits\":\"3704\",\"expiration_month\":7,\"expiration_year\":2030,\"date_created\":\"2020-04-14T03:23:38.000-04:00\",\"date_last_updated\":\"2020-04-14T03:23:38.000-04:00\",\"cardholder\":{\"name\":\"Hermoine Grangerr\",\"identification\":{\"number\":null,\"type\":null}}},\"notification_url\":null,\"refunds\":[],\"processing_mode\":\"aggregator\",\"merchant_account_id\":null,\"acquirer\":null,\"merchant_number\":null,\"acquirer_reconciliation\":[]}", + headers: [ + {"Content-Type", "application/json;charset=UTF-8"}, + {"Transfer-Encoding", "chunked"}, + {"Connection", "keep-alive"}, + {"Cache-Control", "max-age=0"}, + {"ETag", "6d6d1b4a79c868769305de14687c4d6d"}, + {"Vary", "Accept,Accept-Encoding,Accept-Encoding"}, + {"X-Caller-Id", "543713181"}, + {"X-Response-Status", "approved/accredited"}, + {"X-Site-Id", "MLB"}, + {"X-Content-Type-Options", "nosniff"}, + {"X-Request-Id", "59257cf5-6cc6-4afc-b310-e36e412ad4fc"}, + {"X-XSS-Protection", "1; mode=block"}, + {"Strict-Transport-Security", "max-age=16070400; includeSubDomains; preload"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Headers", "Content-Type"}, + {"Access-Control-Allow-Methods", "PUT, GET, POST, DELETE, OPTIONS"}, + {"Access-Control-Max-Age", "86400"}, + {"Timing-Allow-Origin", "*"}, + {"Date", "Mon, 25 Dec 2017 14:17:56 GMT"} + ], + request_url: "#{@base_url}/v1/payments", + status_code: 200 + }} + end + + def bad_card_purchase_response do + {:error, + %HTTPoison.Error{ + reason: "Bad Card for Purchase", + id: 1_234_567 + }} + end + + def bad_amount_purchase_response do + {:error, + %HTTPoison.Response{ + body: + "{\"id\": 20359978, \"date_created\": \"2019-07-10T10:47:58.000-04:00\", \"date_approved\": \"2019-07-10T10:47:58.000-04:00\", \"date_last_updated\": \"2019-07-10T10:47:58.000-04:00\", \"date_of_expiration\": null, \"money_release_date\": \"2019-07-24T10:47:58.000-04:00\", \"operation_type\": \"regular_payment\", \"issuer_id\": \"25\", \"payment_method_id\": \"visa\", \"payment_type_id\": \"credit_card\", \"status\": \"approved\"}", + headers: [ + {"Content-Type", "application/json"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Date", "Mon, 25 Dec 2017 14:17:56 GMT"} + ], + request_url: "#{@base_url}/v1/payments", + status_code: 200 + }} + end + + # authorize mock response + def successful_authorize_response do + {:ok, + %HTTPoison.Response{ + body: + "{\"id\": 20359978, \"date_created\": \"2019-07-10T10:47:58.000-04:00\", \"date_approved\": \"2019-07-10T10:47:58.000-04:00\", \"date_last_updated\": \"2019-07-10T10:47:58.000-04:00\", \"date_of_expiration\": null, \"money_release_date\": \"2019-07-24T10:47:58.000-04:00\", \"operation_type\": \"regular_payment\", \"issuer_id\": \"25\", \"payment_method_id\": \"visa\", \"payment_type_id\": \"credit_card\", \"status\": \"approved\"}", + headers: [ + {"Content-Type", "application/json"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Date", "Mon, 25 Dec 2017 14:17:56 GMT"} + ], + request_url: "#{@base_url}/v1/payments", + status_code: 200 + }} + end + + def bad_card_authorize_response do + {:ok, + %HTTPoison.Response{ + body: + "{\"id\": 20359978, \"date_created\": \"2019-07-10T10:47:58.000-04:00\", \"date_approved\": \"2019-07-10T10:47:58.000-04:00\", \"date_last_updated\": \"2019-07-10T10:47:58.000-04:00\", \"date_of_expiration\": null, \"money_release_date\": \"2019-07-24T10:47:58.000-04:00\", \"operation_type\": \"regular_payment\", \"issuer_id\": \"25\", \"payment_method_id\": \"visa\", \"payment_type_id\": \"credit_card\", \"status\": \"approved\"}", + headers: [ + {"Content-Type", "application/json"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Date", "Mon, 25 Dec 2017 14:17:56 GMT"} + ], + request_url: "#{@base_url}/v1/payments", + status_code: 200 + }} + end + + def bad_amount_authorize_response do + {:ok, + %HTTPoison.Response{ + body: + "{\"id\": 20359978, \"date_created\": \"2019-07-10T10:47:58.000-04:00\", \"date_approved\": \"2019-07-10T10:47:58.000-04:00\", \"date_last_updated\": \"2019-07-10T10:47:58.000-04:00\", \"date_of_expiration\": null, \"money_release_date\": \"2019-07-24T10:47:58.000-04:00\", \"operation_type\": \"regular_payment\", \"issuer_id\": \"25\", \"payment_method_id\": \"visa\", \"payment_type_id\": \"credit_card\", \"status\": \"approved\"}", + headers: [ + {"Content-Type", "application/json"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Date", "Mon, 25 Dec 2017 14:17:56 GMT"} + ], + request_url: "#{@base_url}/v1/payments", + status_code: 200 + }} + end + + # capture mock response + + def successful_capture_response do + {:ok, + %HTTPoison.Response{ + body: + "{\"id\": 20359978, \"date_created\": \"2019-07-10T10:47:58.000-04:00\", \"date_approved\": \"2019-07-10T10:47:58.000-04:00\", \"date_last_updated\": \"2019-07-10T10:47:58.000-04:00\", \"date_of_expiration\": null, \"money_release_date\": \"2019-07-24T10:47:58.000-04:00\", \"operation_type\": \"regular_payment\", \"issuer_id\": \"25\", \"payment_method_id\": \"visa\", \"payment_type_id\": \"credit_card\", \"status\": \"approved\"}", + headers: [ + {"Content-Type", "application/json"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Date", "Mon, 25 Dec 2017 14:17:56 GMT"} + ], + request_url: "#{@base_url}/v1/payments", + status_code: 200 + }} + end + + def bad_id_capture do + {:error, + %HTTPoison.Error{ + reason: "Bad ID for Capture", + id: 1_234_567 + }} + end + + # refund mock response + def successful_refund_response do + {:ok, + %HTTPoison.Response{ + body: + "{\"id\": 20359978, \"payment_id\":24686811, \"unique_sequence_number\":null,\"refund_mode\":\"standard\",\"status\":\"approved\", \"source\":{\"id\":\"543713181\",\"name\":\"Developer Testing\",\"type\":\"collector\"} }", + headers: [ + {"Date", "Mon, 25 Dec 2017 14:17:56 GMT"}, + {"Content-Type", "application/json"}, + {"Transfer-Encoding", "chunked"}, + {"Connection", "keep-alive"}, + {"Cache-Control", "max-age=0"}, + {"ETag", "e2894bf98b818a4f49a3bd1065a3d9b8"}, + {"Vary", "Accept,Accept-Encoding,Accept-Encoding"}, + {"X-Content-Type-Options", "nosniff"}, + {"X-Request-Id", "f6d28d4c-ce70-4cf4-ac82-5733f826eef6"}, + {"X-XSS-Protection", "1; mode=block"}, + {"Strict-Transport-Security", "max-age=16070400; includeSubDomains; preload"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Headers", "Content-Type"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Max-Age", "86400"}, + {"Timing-Allow-Origin", "*"} + ], + request_url: "#{@base_url}/v1/payments", + status_code: 200 + }} + end + + def bad_card_refund do + {:error, + %HTTPoison.Error{ + reason: "Bad Card for refund", + id: 1_234_567 + }} + end + + def debit_less_than_refund do + {:error, + %HTTPoison.Error{ + reason: "Debit less than refund", + id: 1_234_567 + }} + end + + # void mock response + def successful_void do + {:ok, + %HTTPoison.Response{ + body: + "{\"id\": 20359978, \"date_created\": \"2019-07-10T10:47:58.000-04:00\", \"date_approved\": \"2019-07-10T10:47:58.000-04:00\", \"date_last_updated\": \"2019-07-10T10:47:58.000-04:00\", \"date_of_expiration\": null, \"money_release_date\": \"2019-07-24T10:47:58.000-04:00\", \"operation_type\": \"regular_payment\", \"issuer_id\": \"25\", \"payment_method_id\": \"visa\", \"payment_type_id\": \"credit_card\", \"status\": \"approved\"}", + headers: [ + {"Content-Type", "application/json"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Date", "Mon, 25 Dec 2017 14:17:56 GMT"} + ], + request_url: "#{@base_url}/v1/payments", + status_code: 200 + }} + end + + def void_non_existent_id do + {:error, + %HTTPoison.Error{ + reason: "Transaction ID does not exist for Void", + id: 1_234_567 + }} + end + + def customer_payment_profile_success_response do + {:ok, + %HTTPoison.Response{ + body: + "{\"id\": 20359978, \"date_created\": \"2019-07-10T10:47:58.000-04:00\", \"date_approved\": \"2019-07-10T10:47:58.000-04:00\", \"date_last_updated\": \"2019-07-10T10:47:58.000-04:00\", \"date_of_expiration\": null, \"money_release_date\": \"2019-07-24T10:47:58.000-04:00\", \"operation_type\": \"regular_payment\", \"issuer_id\": \"25\", \"payment_method_id\": \"visa\", \"payment_type_id\": \"credit_card\", \"status\": \"approved\"}", + headers: [ + {"Content-Type", "application/json"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Date", "Mon, 25 Dec 2017 14:17:56 GMT"} + ], + request_url: "#{@base_url}/v1/payments", + status_code: 200 + }} + end + + def netwok_error_non_existent_domain do + {:error, %HTTPoison.Error{id: nil, reason: :nxdomain}} + end +end From c48460988595b429d990cf9b801aea6960ff2d49 Mon Sep 17 00:00:00 2001 From: Mandar Vaze Date: Sat, 18 Apr 2020 09:19:08 +0530 Subject: [PATCH 2/2] Fix merge conflict in mix.lock --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index deb35e72..2ac4806d 100644 --- a/mix.lock +++ b/mix.lock @@ -32,7 +32,7 @@ "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm", "5e839994289d60326aa86020c4fbd9c6938af188ecddab2579f07b66cd665328"}, "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm", "7a4c8e1115a2732a67d7624e28cf6c9f30c66711a9e92928e745c255887ba465"}, - "mock": {:hex, :mock, "0.3.1", "994f00150f79a0ea50dc9d86134cd9ebd0d177ad60bd04d1e46336cdfdb98ff9", [:mix], [{:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "dda3bf758218e7c617 + "mock": {:hex, :mock, "0.3.1", "994f00150f79a0ea50dc9d86134cd9ebd0d177ad60bd04d1e46336cdfdb98ff9", [:mix], [{:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "dda3bf758218e7c6179c658b7a12f01271e68b2551c1bfdd70b8ea2178ef8a6e"}, "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"}, "parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm", "578b1d484720749499db5654091ddac818ea0b6d568f2c99c562d2a6dd4aa117"}, "plug": {:hex, :plug, "1.5.1", "1ff35bdecfb616f1a2b1c935ab5e4c47303f866cb929d2a76f0541e553a58165", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.3", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "aa312d5df0f815eed879aaa77543534e21c78ca4a7e983083698fc190796fd9c"},