From f397f4216d21b3a220d50418c50601f41e1fa4e9 Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Tue, 6 Feb 2018 17:27:33 +0530 Subject: [PATCH 01/24] Introduces `Response.t` with docs (#119) * Fixes #1: Introducing `Response.t` with docs * [monei] Adapted for new `Response.t` * Refactored `commit`, `respond` for readability * [monei] Updated test cases * Corrected specs * [bogus] Adapted for Response.t --- lib/gringotts/credit_card.ex | 2 +- lib/gringotts/gateways/bogus.ex | 4 +- lib/gringotts/gateways/monei.ex | 128 ++++++++++++----------- lib/gringotts/response.ex | 83 +++++++++++---- mix.lock | 4 + test/gateways/bogus_test.exs | 16 +-- test/gateways/monei_test.exs | 23 ++-- test/integration/gateways/monei_test.exs | 12 +-- 8 files changed, 163 insertions(+), 109 deletions(-) diff --git a/lib/gringotts/credit_card.ex b/lib/gringotts/credit_card.ex index 32a2a50c..01811e00 100644 --- a/lib/gringotts/credit_card.ex +++ b/lib/gringotts/credit_card.ex @@ -1,6 +1,6 @@ defmodule Gringotts.CreditCard do @moduledoc """ - Defines a `Struct` for (credit) cards and some utilities. + Defines a `struct` for (credit) cards and some utilities. """ defstruct [:number, :month, :year, :first_name, :last_name, :verification_code, :brand] diff --git a/lib/gringotts/gateways/bogus.ex b/lib/gringotts/gateways/bogus.ex index 14140f39..de903744 100644 --- a/lib/gringotts/gateways/bogus.ex +++ b/lib/gringotts/gateways/bogus.ex @@ -28,10 +28,10 @@ defmodule Gringotts.Gateways.Bogus do do: success(customer_id) defp success, - do: {:ok, Response.success(authorization: random_string())} + do: {:ok, Response.success(id: random_string())} defp success(id), - do: {:ok, Response.success(authorization: id)} + do: {:ok, Response.success(id: id)} defp random_string(length \\ 10), do: 1..length |> Enum.map(&random_char/1) |> Enum.join diff --git a/lib/gringotts/gateways/monei.ex b/lib/gringotts/gateways/monei.ex index 53ebb130..7eeca41b 100644 --- a/lib/gringotts/gateways/monei.ex +++ b/lib/gringotts/gateways/monei.ex @@ -258,7 +258,7 @@ defmodule Gringotts.Gateways.Monei do iex> auth_result.id # This is the authorization ID iex> auth_result.token # This is the registration ID/token """ - @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} + @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response.t()} def authorize(amount, %CreditCard{} = card, opts) do {currency, value} = Money.to_string(amount) @@ -293,7 +293,7 @@ defmodule Gringotts.Gateways.Monei do iex> amount = %{value: Decimal.new(35), currency: "USD"} iex> {:ok, capture_result} = Gringotts.capture(Gringotts.Gateways.Monei, amount, auth_result.id, opts) """ - @spec capture(String.t(), Money.t(), keyword) :: {:ok | :error, Response} + @spec capture(String.t(), Money.t(), keyword) :: {:ok | :error, Response.t()} def capture(payment_id, amount, opts) def capture(<>, amount, opts) do @@ -329,7 +329,7 @@ defmodule Gringotts.Gateways.Monei do iex> {:ok, purchase_result} = Gringotts.purchase(Gringotts.Gateways.Monei, amount, card, opts) iex> purchase_result.token # This is the registration ID/token """ - @spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} + @spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response.t()} def purchase(amount, %CreditCard{} = card, opts) do {currency, value} = Money.to_string(amount) @@ -361,7 +361,7 @@ defmodule Gringotts.Gateways.Monei do iex> amount = %{value: Decimal.new(42), currency: "USD"} iex> {:ok, refund_result} = Gringotts.refund(Gringotts.Gateways.Monei, purchase_result.id, amount) """ - @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response} + @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response.t()} def refund(amount, <>, opts) do {currency, value} = Money.to_string(amount) @@ -396,7 +396,7 @@ defmodule Gringotts.Gateways.Monei do iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} iex> {:ok, store_result} = Gringotts.store(Gringotts.Gateways.Monei, card, []) """ - @spec store(CreditCard.t(), keyword) :: {:ok | :error, Response} + @spec store(CreditCard.t(), keyword) :: {:ok | :error, Response.t()} def store(%CreditCard{} = card, opts) do params = card_params(card) commit(:post, "registrations", params, opts) @@ -409,7 +409,7 @@ defmodule Gringotts.Gateways.Monei do Deletes previously stored payment-source data. """ - @spec unstore(String.t(), keyword) :: {:ok | :error, Response} + @spec unstore(String.t(), keyword) :: {:ok | :error, Response.t()} def unstore(registration_id, opts) def unstore(<>, opts) do @@ -447,7 +447,7 @@ defmodule Gringotts.Gateways.Monei do iex> {:ok, void_result} = Gringotts.void(Gringotts.Gateways.Monei, auth_result.id, opts) """ - @spec void(String.t(), keyword) :: {:ok | :error, Response} + @spec void(String.t(), keyword) :: {:ok | :error, Response.t()} def void(payment_id, opts) def void(<>, opts) do @@ -466,72 +466,101 @@ defmodule Gringotts.Gateways.Monei do ] end - # Makes the request to MONEI's network. - @spec commit(atom, String.t(), keyword, keyword) :: {:ok | :error, Response} - defp commit(method, endpoint, params, opts) do - auth_params = [ + defp auth_params(opts) do + [ "authentication.userId": opts[:config][:userId], "authentication.password": opts[:config][:password], "authentication.entityId": opts[:config][:entityId] ] + end + + # Makes the request to MONEI's network. + @spec commit(atom, String.t(), keyword, keyword) :: {:ok | :error, Response.t()} + defp commit(:post, endpoint, params, opts) do url = "#{base_url(opts)}/#{version(opts)}/#{endpoint}" case expand_params(opts, params[:paymentType]) do {:error, reason} -> - {:error, Response.error(description: reason)} + {:error, Response.error(reason: reason)} validated_params -> - network_response = - case method do - :post -> - HTTPoison.post( - url, - {:form, params ++ validated_params ++ auth_params}, - @default_headers - ) - - :delete -> - HTTPoison.delete(url <> "?" <> URI.encode_query(auth_params)) - end - - respond(network_response) + url + |> HTTPoison.post({:form, params ++ validated_params ++ auth_params(opts)}, @default_headers) + |> respond end end + # This clause is only used by `unstore/2` + defp commit(:delete, endpoint, _params, opts) do + base_url = "#{base_url(opts)}/#{version(opts)}/#{endpoint}" + auth_params = auth_params(opts) + query_string = auth_params |> URI.encode_query() + + base_url <> "?" <> query_string + |> HTTPoison.delete() + |> respond + end + # Parses MONEI's response and returns a `Gringotts.Response` struct in a # `:ok`, `:error` tuple. - @spec respond(term) :: {:ok | :error, Response} + @spec respond(term) :: {:ok | :error, Response.t()} defp respond(monei_response) defp respond({:ok, %{status_code: 200, body: body}}) do - case decode(body) do - {:ok, decoded_json} -> - case parse_response(decoded_json) do - {:ok, results} -> {:ok, Response.success([{:id, decoded_json["id"]} | results])} - {:error, errors} -> {:ok, Response.error([{:id, decoded_json["id"]} | errors])} - end + common = [raw: body, status_code: 200] + + with {:ok, decoded_json} <- decode(body), + {:ok, results} <- parse_response(decoded_json) do + {:ok, Response.success(common ++ results)} + else + {:not_ok, errors} -> + {:ok, Response.error(common ++ errors)} {:error, _} -> - {:error, Response.error(raw: body, code: :undefined_response_from_monei)} + {:error, Response.error([reason: "undefined response from monei"] ++ common)} end end defp respond({:ok, %{status_code: status_code, body: body}}) do - {:error, Response.error(code: status_code, raw: body)} + {:error, Response.error(status_code: status_code, raw: body)} end defp respond({:error, %HTTPoison.Error{} = error}) do { :error, Response.error( - code: error.id, reason: "network related failure", - description: "HTTPoison says '#{error.reason}'" + message: "HTTPoison says '#{error.reason}' [ID: #{error.id || "nil"}]" ) } end + defp parse_response(%{"result" => result} = data) do + {address, zip_code} = @avs_code_translator[result["avsResponse"]] + + results = [ + id: data["id"], + token: data["registrationId"], + gateway_code: result["code"], + message: result["description"], + fraud_review: data["risk"], + cvc_result: @cvc_code_translator[result["cvvResponse"]], + avs_result: %{address: address, zip_code: zip_code} + ] + + non_nil_params = Enum.filter(results, fn {_, v} -> v != nil end) + verify(non_nil_params) + end + + defp verify(results) do + if String.match?(results[:gateway_code], ~r{^(000\.000\.|000\.100\.1|000\.[36])}) do + {:ok, results} + else + {:not_ok, [{:reason, results[:message]} | results]} + end + end + defp expand_params(params, action_type) do Enum.reduce_while(params, [], fn {k, v}, acc -> case k do @@ -587,31 +616,6 @@ defmodule Gringotts.Gateways.Monei do currency in @supported_currencies end - defp parse_response(%{"result" => result} = data) do - {address, zip_code} = @avs_code_translator[result["avsResponse"]] - - results = [ - code: result["code"], - description: result["description"], - risk: data["risk"]["score"], - cvc_result: @cvc_code_translator[result["cvvResponse"]], - avs_result: [address: address, zip_code: zip_code], - raw: data, - token: data["registrationId"] - ] - - filtered = Enum.filter(results, fn {_, v} -> v != nil end) - verify(filtered) - end - - defp verify(results) do - if String.match?(results[:code], ~r{^(000\.000\.|000\.100\.1|000\.[36])}) do - {:ok, results} - else - {:error, [{:reason, results[:description]} | results]} - end - end - defp make(prefix, param) do Enum.into(param, [], fn {k, v} -> {"#{prefix}.#{k}", v} end) end diff --git a/lib/gringotts/response.ex b/lib/gringotts/response.ex index f9097490..ac369f89 100644 --- a/lib/gringotts/response.ex +++ b/lib/gringotts/response.ex @@ -1,26 +1,73 @@ defmodule Gringotts.Response do - @moduledoc ~S""" - Module which defines the struct for response struct. - - Response struct is a standard response from public API to the application. - - It mostly has such as:- - * `success`: boolean indicating the status of the transaction - * `authorization`: token which is used to issue requests without the card info - * `status_code`: response code - * `error_code`: error code if there is error else nil - * `message`: message related to the status of the response - * `avs_result`: result for address verfication - * `cvc_result`: result for cvc verification - * `params`: original raw response from the gateway - * `fraud_review`: information related to fraudulent transactions + @moduledoc """ + Defines the Response `struct` and some utilities. + + All `Gringotts` public API calls will return a `Response.t` wrapped in an + `:ok` or `:error` `tuple`. It is guaranteed that an `:ok` will be returned + only when the request succeeds at the gateway, ie, no error occurs. """ - + defstruct [ - :success, :authorization, :status_code, :error_code, :message, - :avs_result, :cvc_result, :params, :fraud_review + :success, :id, :token, :status_code, :gateway_code, :reason, :message, + :avs_result, :cvc_result, :raw, :fraud_review ] + @typedoc """ + The standard Response from `Gringotts`. + + | Field | Type | Description | + |----------------|-------------------|---------------------------------------| + | `success` | `boolean` | Indicates the status of the\ + transaction. | + | `id` | `String.t` | Gateway supplied identifier of the\ + transaction. | + | `token` | `String.t` | Gateway supplied `token`. _This is\ + different from `Response.id`_. | + | `status_code` | `non_neg_integer` | `HTTP` response code. | + | `gateway_code` | `String.t` | Gateway's response code "as-is". | + | `message` | `String.t` | String describing the response status.| + | `avs_result` | `map` | Address Verification Result.\ + Schema: `%{street: String.t,\ + zip_code: String.t}` | + | `cvc_result` | `String.t` | Result of the [CVC][cvc] validation. | + | `reason` | `String.t` | Explain the `reason` of error, in\ + case of error. `nil` otherwise. | + | `raw` | `String.t` | Raw response from the gateway. | + | `fraud_review` | `term` | Gateway's risk assessment of the\ + transaction. | + + ## Notes + + 1. It is not guaranteed that all fields will be populated for all calls, and + some gateways might insert non-standard fields. Please refer the Gateways' + docs for that information. + + 2. `success` is deprecated in `v1.1.0` and will be removed in `v1.2.0`. + + 3. For some actions the Gateway returns an additional token, say as reponse to + a customer tokenization/registration. In such cases the `id` is not + useable because it refers to the transaction, the `token` is. + + > On the other hand for authorizations or captures, there's no `token`. + + 4. The schema of `fraud_review` is Gateway specific. + + [cvc]: https://en.wikipedia.org/wiki/Card_security_code + """ + @type t:: %__MODULE__{ + success: boolean, + id: String.t, + token: String.t, + status_code: non_neg_integer, + gateway_code: String.t, + reason: String.t, + message: String.t, + avs_result: %{street: String.t, zip_code: String.t}, + cvc_result: String.t, + raw: String.t, + fraud_review: term + } + def success(opts \\ []) do new(true, opts) end diff --git a/mix.lock b/mix.lock index df5aabfe..e3441847 100644 --- a/mix.lock +++ b/mix.lock @@ -2,6 +2,7 @@ "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.0.0", "a0c0e475107135f76b8c1d5bc7efb33cd3815cb3cf3dea7aefdd174dabead064", [:rebar3], [], "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.8.10", "261862bb7363247762e1063713bb85df2bbd84af8d8610d1272cd9c1943bba63", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}], "hexpm"}, @@ -16,6 +17,7 @@ "ex_money": {:hex, :ex_money, "1.1.2", "4336192f1ac263900dfb4f63c1f71bc36a7cdee5d900e81937d3213be3360f9f", [: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"}, "excoveralls": {:hex, :excoveralls, "0.8.0", "99d2691d3edf8612f128be3f9869c4d44b91c67cec92186ce49470ae7a7404cf", [: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"}, + "gettext": {:hex, :gettext, "0.14.0", "1a019a2e51d5ad3d126efe166dcdf6563768e5d06c32a99ad2281a1fa94b4c72", [:mix], [], "hexpm"}, "hackney": {:hex, :hackney, "1.10.1", "c38d0ca52ea80254936a32c45bb7eb414e7a96a521b4ce76d00a69753b157f21", [: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"}, "idna": {:hex, :idna, "5.1.0", "d72b4effeb324ad5da3cab1767cb16b17939004e789d8c0ad5b70f3cea20c89a", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, @@ -30,5 +32,7 @@ "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.1.25", "6002dae5432f749d1c93e2cd103eb73cecb53e50d2c885349e8e4146fc96bd44", [: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, "0.1.2", "b48ab9ed0a24f43a6061e0c21deda88b966a2121af5c445d4fc550dd822e23dc", [:mix], [], "hexpm"}} diff --git a/test/gateways/bogus_test.exs b/test/gateways/bogus_test.exs index b7c10ebd..5810dfc0 100644 --- a/test/gateways/bogus_test.exs +++ b/test/gateways/bogus_test.exs @@ -5,35 +5,35 @@ defmodule Gringotts.Gateways.BogusTest do alias Gringotts.Gateways.Bogus, as: Gateway test "authorize" do - {:ok, %Response{authorization: authorization, success: success}} = + {:ok, %Response{id: id, success: success}} = Gateway.authorize(10.95, :card, []) assert success - assert authorization != nil + assert id != nil end test "purchase" do - {:ok, %Response{authorization: authorization, success: success}} = + {:ok, %Response{id: id, success: success}} = Gateway.purchase(10.95, :card, []) assert success - assert authorization != nil + assert id != nil end test "capture" do - {:ok, %Response{authorization: authorization, success: success}} = + {:ok, %Response{id: id, success: success}} = Gateway.capture(1234, 5, []) assert success - assert authorization != nil + assert id != nil end test "void" do - {:ok, %Response{authorization: authorization, success: success}} = + {:ok, %Response{id: id, success: success}} = Gateway.void(1234, []) assert success - assert authorization != nil + assert id != nil end test "store" do diff --git a/test/gateways/monei_test.exs b/test/gateways/monei_test.exs index 9fd72895..8d3a11ec 100644 --- a/test/gateways/monei_test.exs +++ b/test/gateways/monei_test.exs @@ -119,7 +119,7 @@ defmodule Gringotts.Gateways.MoneiTest do describe "core" do test "with unsupported currency.", %{auth: auth} do {:error, response} = Gateway.authorize(@bad_currency, @card, config: auth) - assert response.description == "Invalid currency" + assert response.reason == "Invalid currency" end test "when MONEI is down or unreachable.", %{bypass: bypass, auth: auth} do @@ -161,7 +161,7 @@ defmodule Gringotts.Gateways.MoneiTest do opts = randoms ++ @extra_opts ++ [config: auth] {:ok, response} = Gateway.purchase(@amount42, @card, opts) - assert response.code == "000.100.110" + assert response.gateway_code == "000.100.110" assert response.token == "8a82944a60e09c550160e92da144491e" end @@ -181,7 +181,7 @@ defmodule Gringotts.Gateways.MoneiTest do end) {:ok, response} = Gateway.authorize(@amount42, @card, config: auth) - assert response.code == "000.100.110" + assert response.gateway_code == "000.100.110" end end @@ -192,7 +192,7 @@ defmodule Gringotts.Gateways.MoneiTest do end) {:ok, response} = Gateway.purchase(@amount42, @card, config: auth) - assert response.code == "000.100.110" + assert response.gateway_code == "000.100.110" end test "with createRegistration.", %{bypass: bypass, auth: auth} do @@ -203,7 +203,7 @@ defmodule Gringotts.Gateways.MoneiTest do end) {:ok, response} = Gateway.purchase(@amount42, @card, register: true, config: auth) - assert response.code == "000.100.110" + assert response.gateway_code == "000.100.110" assert response.token == "8a82944a60e09c550160e92da144491e" end end @@ -215,8 +215,7 @@ defmodule Gringotts.Gateways.MoneiTest do end) {:ok, response} = Gateway.store(@card, config: auth) - assert response.code == "000.100.110" - assert response.raw["card"]["holder"] == "Jo Doe" + assert response.gateway_code == "000.100.110" end end @@ -234,7 +233,7 @@ defmodule Gringotts.Gateways.MoneiTest do {:ok, response} = Gateway.capture("7214344242e11af79c0b9e7b4f3f6234", @amount42, config: auth) - assert response.code == "000.100.110" + assert response.gateway_code == "000.100.110" end test "with createRegistration that is ignored", %{bypass: bypass, auth: auth} do @@ -257,7 +256,7 @@ defmodule Gringotts.Gateways.MoneiTest do config: auth ) - assert response.code == "000.100.110" + assert response.gateway_code == "000.100.110" end end @@ -274,7 +273,7 @@ defmodule Gringotts.Gateways.MoneiTest do {:ok, response} = Gateway.refund(@amount3, "7214344242e11af79c0b9e7b4f3f6234", config: auth) - assert response.code == "000.100.110" + assert response.gateway_code == "000.100.110" end end @@ -290,7 +289,7 @@ defmodule Gringotts.Gateways.MoneiTest do ) {:error, response} = Gateway.unstore("7214344242e11af79c0b9e7b4f3f6234", config: auth) - assert response.code == :undefined_response_from_monei + assert response.reason == "undefined response from monei" end end @@ -306,7 +305,7 @@ defmodule Gringotts.Gateways.MoneiTest do ) {:ok, response} = Gateway.void("7214344242e11af79c0b9e7b4f3f6234", config: auth) - assert response.code == "000.100.110" + assert response.gateway_code == "000.100.110" end end diff --git a/test/integration/gateways/monei_test.exs b/test/integration/gateways/monei_test.exs index 6619a43f..3066ff62 100644 --- a/test/integration/gateways/monei_test.exs +++ b/test/integration/gateways/monei_test.exs @@ -85,9 +85,9 @@ defmodule Gringotts.Integration.Gateways.MoneiTest do test "authorize", %{opts: opts} do case Gringotts.authorize(Gateway, @amount, @card, opts) do {:ok, response} -> - assert response.code == "000.100.110" + assert response.gateway_code == "000.100.110" - assert response.description == + assert response.message == "Request successfully processed in 'Merchant in Integrator Test Mode'" assert String.length(response.id) == 32 @@ -101,9 +101,9 @@ defmodule Gringotts.Integration.Gateways.MoneiTest do test "capture", %{opts: _opts} do case Gringotts.capture(Gateway, "s", @amount) do {:ok, response} -> - assert response.code == "000.100.110" + assert response.gateway_code == "000.100.110" - assert response.description == + assert response.message == "Request successfully processed in 'Merchant in Integrator Test Mode'" assert String.length(response.id) == 32 @@ -116,9 +116,9 @@ defmodule Gringotts.Integration.Gateways.MoneiTest do test "purchase", %{opts: opts} do case Gringotts.purchase(Gateway, @amount, @card, opts) do {:ok, response} -> - assert response.code == "000.100.110" + assert response.gateway_code == "000.100.110" - assert response.description == + assert response.message == "Request successfully processed in 'Merchant in Integrator Test Mode'" assert String.length(response.id) == 32 From 57c0dbb6508d4385b9358dab0a0a839c70f866f9 Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Mon, 5 Feb 2018 14:42:10 +0530 Subject: [PATCH 02/24] Removes payment worker. * Gringotts does not start any process now * Removed `adapter` key from config as it was redundant. * Updated docs and mix task. --- README.md | 10 --- lib/gringotts.ex | 38 +++++----- lib/gringotts/adapter.ex | 2 +- lib/gringotts/application.ex | 33 --------- lib/gringotts/gateways/authorize_net.ex | 1 - lib/gringotts/gateways/cams.ex | 1 - lib/gringotts/gateways/global_collect.ex | 1 - lib/gringotts/gateways/monei.ex | 1 - lib/gringotts/gateways/paymill.ex | 1 - lib/gringotts/gateways/stripe.ex | 1 - lib/gringotts/gateways/trexle.ex | 1 - lib/gringotts/worker.ex | 89 ------------------------ mix.exs | 1 - templates/gateway.eex | 1 - templates/integration.eex | 3 +- test/gateways/cams_test.exs | 5 +- test/gringotts_test.exs | 5 +- test/integration/gateways/monei_test.exs | 1 - 18 files changed, 23 insertions(+), 172 deletions(-) delete mode 100644 lib/gringotts/application.ex delete mode 100644 lib/gringotts/worker.ex diff --git a/README.md b/README.md index 42e56c2f..6b71deb4 100644 --- a/README.md +++ b/README.md @@ -33,15 +33,6 @@ def deps do end ``` -Add gringotts to the list of applications to be started. -```elixir -def application do - [ - extra_applications: [:gringotts] - ] -end -``` - ## Usage This simple example demonstrates how a purchase can be made using a person's credit card details. @@ -50,7 +41,6 @@ Add configs in `config/config.exs` file. ```elixir config :gringotts, Gringotts.Gateways.Monei, - adapter: Gringotts.Gateways.Monei, userId: "your_secret_user_id", password: "your_secret_password", entityId: "your_secret_channel_id" diff --git a/lib/gringotts.ex b/lib/gringotts.ex index 13d0eb07..d4f9e65e 100644 --- a/lib/gringotts.ex +++ b/lib/gringotts.ex @@ -133,13 +133,8 @@ defmodule Gringotts do following format: config :gringotts, Gringotts.Gateways.XYZ, - adapter: Gringotts.Gateways.XYZ, # some_documented_key: associated_value # some_other_key: another_value - - > ***Note!*** - > The config key matches the `:adapter`! Both ***must*** be the Gateway module - > name! """ import GenServer, only: [call: 2] @@ -166,8 +161,8 @@ defmodule Gringotts do {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.XYZ, amount, card, opts) """ def authorize(gateway, amount, card, opts \\ []) do - validate_config(gateway) - call(:payment_worker, {:authorize, gateway, amount, card, opts}) + config = get_and_validate_config(gateway) + gateway.authorize(amount, card, [{:config, config} | opts]) end @doc """ @@ -189,8 +184,8 @@ defmodule Gringotts do Gringotts.capture(Gringotts.Gateways.XYZ, amount, auth_result.id, opts) """ def capture(gateway, id, amount, opts \\ []) do - validate_config(gateway) - call(:payment_worker, {:capture, gateway, id, amount, opts}) + config = get_and_validate_config(gateway) + gateway.capture(id, amount, [{:config, config} | opts]) end @doc """ @@ -217,8 +212,8 @@ defmodule Gringotts do Gringotts.purchase(Gringotts.Gateways.XYZ, amount, card, opts) """ def purchase(gateway, amount, card, opts \\ []) do - validate_config(gateway) - call(:payment_worker, {:purchase, gateway, amount, card, opts}) + config = get_and_validate_config(gateway) + gateway.purchase(amount, card, [{:config, config} | opts]) end @doc """ @@ -237,8 +232,8 @@ defmodule Gringotts do Gringotts.purchase(Gringotts.Gateways.XYZ, amount, id, opts) """ def refund(gateway, amount, id, opts \\ []) do - validate_config(gateway) - call(:payment_worker, {:refund, gateway, amount, id, opts}) + config = get_and_validate_config(gateway) + gateway.refund(amount, id, [{:config, config} | opts]) end @doc """ @@ -258,8 +253,8 @@ defmodule Gringotts do Gringotts.store(Gringotts.Gateways.XYZ, card, opts) """ def store(gateway, card, opts \\ []) do - validate_config(gateway) - call(:payment_worker, {:store, gateway, card, opts}) + config = get_and_validate_config(gateway) + gateway.store(card, [{:config, config} | opts]) end @doc """ @@ -276,8 +271,8 @@ defmodule Gringotts do Gringotts.unstore(Gringotts.Gateways.XYZ, token) """ def unstore(gateway, token, opts \\ []) do - validate_config(gateway) - call(:payment_worker, {:unstore, gateway, token, opts}) + config = get_and_validate_config(gateway) + gateway.unstore(token, [{:config, config} | opts]) end @doc """ @@ -297,13 +292,16 @@ defmodule Gringotts do Gringotts.void(Gringotts.Gateways.XYZ, id, opts) """ def void(gateway, id, opts \\ []) do - validate_config(gateway) - call(:payment_worker, {:void, gateway, id, opts}) + config = get_and_validate_config(gateway) + gateway.void(id, [{:config, config} | opts]) end - defp validate_config(gateway) do + defp get_and_validate_config(gateway) do # Keep the key name and adapter the same in the config in application config = Application.get_env(:gringotts, gateway) + # The following call to validate_config might raise an error gateway.validate_config(config) + global_config = Application.get_env(:gringotts, :global_config) || [mode: :test] + Keyword.merge(global_config, config) end end diff --git a/lib/gringotts/adapter.ex b/lib/gringotts/adapter.ex index d6e250b6..8d2a24f8 100644 --- a/lib/gringotts/adapter.ex +++ b/lib/gringotts/adapter.ex @@ -31,4 +31,4 @@ defmodule Gringotts.Adapter do end end end -end \ No newline at end of file +end diff --git a/lib/gringotts/application.ex b/lib/gringotts/application.ex deleted file mode 100644 index f852de3b..00000000 --- a/lib/gringotts/application.ex +++ /dev/null @@ -1,33 +0,0 @@ -defmodule Gringotts.Application do - @moduledoc ~S""" - Has the supervision tree which monitors all the workers - that are handling the payments. - """ - use Application - - def start(_type, _args) do - import Supervisor.Spec, warn: false - app_config = Application.get_all_env(:gringotts) - adapters = Enum.filter(app_config, fn({_, klist}) -> klist != [] end) - |> Enum.map(fn({_, klist}) -> Keyword.get(klist, :adapter) end) - - children = [ - # Define workers and child supervisors to be supervised - # worker(Gringotts.Worker, [arg1, arg2, arg3]) - worker( - Gringotts.Worker, - [ - adapters, # gateways - app_config, # options(config from application) - # Since we just have one worker handling all the incoming - # requests so this name remains fixed - [name: :payment_worker] - ]) - ] - - # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html - # for other strategies and supported options - opts = [strategy: :one_for_one, name: Gringotts.Supervisor] - Supervisor.start_link(children, opts) - end -end diff --git a/lib/gringotts/gateways/authorize_net.ex b/lib/gringotts/gateways/authorize_net.ex index 04762db3..5e2b57fd 100644 --- a/lib/gringotts/gateways/authorize_net.ex +++ b/lib/gringotts/gateways/authorize_net.ex @@ -65,7 +65,6 @@ defmodule Gringotts.Gateways.AuthorizeNet do fields** and would look something like this: config :gringotts, Gringotts.Gateways.AuthorizeNet, - adapter: Gringotts.Gateways.AuthorizeNet, name: "name_provided_by_authorize_net", transaction_key: "transactionKey_provided_by_authorize_net" diff --git a/lib/gringotts/gateways/cams.ex b/lib/gringotts/gateways/cams.ex index 7e37dd05..1787e947 100644 --- a/lib/gringotts/gateways/cams.ex +++ b/lib/gringotts/gateways/cams.ex @@ -56,7 +56,6 @@ defmodule Gringotts.Gateways.Cams do > fields** and would look something like this: config :gringotts, Gringotts.Gateways.Cams, - adapter: Gringotts.Gateways.Cams, username: "your_secret_user_name", password: "your_secret_password", diff --git a/lib/gringotts/gateways/global_collect.ex b/lib/gringotts/gateways/global_collect.ex index 0a2010e6..0c6d143e 100644 --- a/lib/gringotts/gateways/global_collect.ex +++ b/lib/gringotts/gateways/global_collect.ex @@ -51,7 +51,6 @@ defmodule Gringotts.Gateways.GlobalCollect do something like this: config :gringotts, Gringotts.Gateways.GlobalCollect, - adapter: Gringotts.Gateways.GlobalCollect, secret_api_key: "your_secret_secret_api_key" api_key_id: "your_secret_api_key_id" merchant_id: "your_secret_merchant_id" diff --git a/lib/gringotts/gateways/monei.ex b/lib/gringotts/gateways/monei.ex index 7eeca41b..c297a1a3 100644 --- a/lib/gringotts/gateways/monei.ex +++ b/lib/gringotts/gateways/monei.ex @@ -73,7 +73,6 @@ defmodule Gringotts.Gateways.Monei do fields** and would look something like this: config :gringotts, Gringotts.Gateways.Monei, - adapter: Gringotts.Gateways.Monei, userId: "your_secret_user_id", password: "your_secret_password", entityId: "your_secret_channel_id" diff --git a/lib/gringotts/gateways/paymill.ex b/lib/gringotts/gateways/paymill.ex index 7a05bca1..8b00ba3e 100644 --- a/lib/gringotts/gateways/paymill.ex +++ b/lib/gringotts/gateways/paymill.ex @@ -22,7 +22,6 @@ defmodule Gringotts.Gateways.Paymill do Your application config must include 'private_key', 'public_key' config :gringotts, Gringotts.Gateways.Paymill, - adapter: Gringotts.Gateways.Paymill, private_key: "your_privat_key", public_key: "your_public_key" """ diff --git a/lib/gringotts/gateways/stripe.ex b/lib/gringotts/gateways/stripe.ex index 1a0d2b89..7c1befd2 100644 --- a/lib/gringotts/gateways/stripe.ex +++ b/lib/gringotts/gateways/stripe.ex @@ -51,7 +51,6 @@ defmodule Gringotts.Gateways.Stripe do Your Application config must look something like this: config :gringotts, Gringotts.Gateways.Stripe, - adapter: Gringotts.Gateways.Stripe, secret_key: "your_secret_key", default_currency: "usd" """ diff --git a/lib/gringotts/gateways/trexle.ex b/lib/gringotts/gateways/trexle.ex index d89d7767..fba77838 100644 --- a/lib/gringotts/gateways/trexle.ex +++ b/lib/gringotts/gateways/trexle.ex @@ -41,7 +41,6 @@ defmodule Gringotts.Gateways.Trexle do Your Application config must look something like this: config :gringotts, Gringotts.Gateways.Trexle, - adapter: Gringotts.Gateways.Trexle, api_key: "your-secret-API-key" [dashboard]: https://trexle.com/dashboard/ diff --git a/lib/gringotts/worker.ex b/lib/gringotts/worker.ex deleted file mode 100644 index 786ad6ed..00000000 --- a/lib/gringotts/worker.ex +++ /dev/null @@ -1,89 +0,0 @@ -defmodule Gringotts.Worker do - @moduledoc ~S""" - A central supervised worker handling all the calls for different gateways - - It's main task is to re-route the requests to the respective gateway methods. - - State for this worker currently is:- - * `gateways`:- a list of all the gateways configured in the application. - * `all_configs`:- All the configurations for all the gateways that are configured. - """ - use GenServer - - def start_link(gateways, all_config, opts \\ []) do - GenServer.start_link(__MODULE__, [gateways, all_config], opts) - end - - def init([gateways, all_config]) do - {:ok, %{configs: all_config, gateways: gateways}} - end - - @doc """ - Handles call for `authorize` method - """ - def handle_call({:authorize, gateway, amount, card, opts}, _from, state) do - {gateway, config} = set_gateway_and_config(gateway) - response = gateway.authorize(amount, card, [{:config, config} | opts]) - {:reply, response, state} - end - - @doc """ - Handles call for `purchase` method - """ - def handle_call({:purchase, gateway, amount, card, opts}, _from, state) do - {gateway, config} = set_gateway_and_config(gateway) - response = gateway.purchase(amount, card, [{:config, config} | opts]) - {:reply, response, state} - end - - @doc """ - Handles call for `capture` method - """ - def handle_call({:capture, gateway, id, amount, opts}, _from, state) do - {gateway, config} = set_gateway_and_config(gateway) - response = gateway.capture(id, amount, [{:config, config} | opts]) - {:reply, response, state} - end - - @doc """ - Handles call for `void` method - """ - def handle_call({:void, gateway, id, opts}, _from, state) do - {gateway, config} = set_gateway_and_config(gateway) - response = gateway.void(id, [{:config, config} | opts]) - {:reply, response, state} - end - - @doc """ - Handles call for 'refund' method - """ - def handle_call({:refund, gateway, amount, id, opts}, _from, state) do - {gateway, config} = set_gateway_and_config(gateway) - response = gateway.refund(amount, id, [{:config, config} | opts]) - {:reply, response, state} - end - - @doc """ - Handles call for `store` method - """ - def handle_call({:store, gateway, card, opts}, _from, state) do - {gateway, config} = set_gateway_and_config(gateway) - response = gateway.store(card, [{:config, config} | opts]) - {:reply, response, state} - end - - @doc """ - Handles call for 'unstore' method - """ - def handle_call({:unstore, gateway, customer_id, opts}, _from, state) do - {gateway, config} = set_gateway_and_config(gateway) - response = gateway.unstore(customer_id, [{:config, config} | opts]) - {:reply, response, state} - end - - defp set_gateway_and_config(request_gateway) do - global_config = Application.get_env(:gringotts, :global_config) || [mode: :test] - gateway_config = Application.get_env(:gringotts, request_gateway) - {request_gateway, Keyword.merge(global_config, gateway_config)} - end -end diff --git a/mix.exs b/mix.exs index 7947413c..36d86864 100644 --- a/mix.exs +++ b/mix.exs @@ -33,7 +33,6 @@ defmodule Gringotts.Mixfile do def application do [ applications: [:httpoison, :hackney, :elixir_xml_to_map, :timex], - mod: {Gringotts.Application, []} ] end diff --git a/templates/gateway.eex b/templates/gateway.eex index b68e0a31..6169bae4 100644 --- a/templates/gateway.eex +++ b/templates/gateway.eex @@ -50,7 +50,6 @@ defmodule Gringotts.Gateways.<%= gateway_module %> do > something like this: > > config :gringotts, Gringotts.Gateways.<%= gateway_module %>, - > adapter: Gringotts.Gateways.<%= gateway_module %><%= if required_config_keys != [] do %>,<% end %> <%= for key <- required_config_keys do %>> <%= "#{key}" %>: "your_secret_<%= "#{key}" %>" <% end %> diff --git a/templates/integration.eex b/templates/integration.eex index f57b4b86..74bc5fc7 100644 --- a/templates/integration.eex +++ b/templates/integration.eex @@ -8,8 +8,7 @@ defmodule Gringotts.Integration.Gateways.<%= gateway_module <> "Test"%> do setup_all do Application.put_env(:gringotts, Gringotts.Gateways.<%= gateway_module%>, - [ - adapter: Gringotts.Gateways.<%= gateway_module%><%= if required_config_keys != [] do %>,<%= for key <- Enum.intersperse(required_config_keys, ",") do %><%= if key === "," do %><%= "#{key}" %><% else %> + [ <%= if required_config_keys == [] do %># some_key: "some_secret_key"<% else %><%= for key <- Enum.intersperse(required_config_keys, ",") do %><%= if key === "," do %><%= "#{key}" %><% else %> <%= "#{key}" %>: "your_secret_<%= "#{key}" %>"<% end %><% end %><% end %> ] ) diff --git a/test/gateways/cams_test.exs b/test/gateways/cams_test.exs index 3c20e545..6faf56b2 100644 --- a/test/gateways/cams_test.exs +++ b/test/gateways/cams_test.exs @@ -57,10 +57,7 @@ defmodule Gringotts.Gateways.CamsTest do @bad_authorization "some_fake_transaction_id" setup_all do - Application.put_env( - :gringotts, - Gateway, - adapter: Gateway, + Application.put_env(:gringotts, Gateway, username: "some_secret_user_name", password: "some_secret_password" ) diff --git a/test/gringotts_test.exs b/test/gringotts_test.exs index d4634e14..7d9f7681 100644 --- a/test/gringotts_test.exs +++ b/test/gringotts_test.exs @@ -4,12 +4,11 @@ defmodule GringottsTest do import Gringotts @test_config [ - adapter: GringottsTest.FakeGateway, some_auth_info: :merchant_secret_key, other_secret: :sun_rises_in_the_east ] - @bad_config [adapter: GringottsTest.FakeGateway, some_auth_info: :merchant_secret_key] + @bad_config [some_auth_info: :merchant_secret_key] defmodule FakeGateway do use Gringotts.Adapter, required_config: [:some_auth_info, :other_secret] @@ -83,7 +82,7 @@ defmodule GringottsTest do assert_raise( ArgumentError, - "expected [:other_secret] to be set, got: [adapter: GringottsTest.FakeGateway, some_auth_info: :merchant_secret_key]\n", + "expected [:other_secret] to be set, got: [some_auth_info: :merchant_secret_key]\n", fn -> authorize(GringottsTest.FakeGateway, 100, :card, []) end ) end diff --git a/test/integration/gateways/monei_test.exs b/test/integration/gateways/monei_test.exs index 3066ff62..8ed929e4 100644 --- a/test/integration/gateways/monei_test.exs +++ b/test/integration/gateways/monei_test.exs @@ -66,7 +66,6 @@ defmodule Gringotts.Integration.Gateways.MoneiTest do Application.put_env( :gringotts, Gringotts.Gateways.Monei, - adapter: Gringotts.Gateways.Monei, userId: "8a8294186003c900016010a285582e0a", password: "hMkqf2qbWf", entityId: "8a82941760036820016010a28a8337f6" From 91e45069730db1fba8b377f742575a184ca01fde Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Mon, 5 Feb 2018 17:38:25 +0530 Subject: [PATCH 03/24] Removed offending (now useless) test case --- test/integration/gateways/monei_test.exs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/integration/gateways/monei_test.exs b/test/integration/gateways/monei_test.exs index 8ed929e4..c9b67aad 100644 --- a/test/integration/gateways/monei_test.exs +++ b/test/integration/gateways/monei_test.exs @@ -126,9 +126,4 @@ defmodule Gringotts.Integration.Gateways.MoneiTest do flunk() end end - - test "Environment setup" do - config = Application.get_env(:gringotts, Gringotts.Gateways.Monei) - assert config[:adapter] == Gringotts.Gateways.Monei - end end From ee9140ddc14fc3212ca48f7b011a4a1dc899e3cc Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Thu, 15 Feb 2018 13:00:33 +0530 Subject: [PATCH 04/24] Removed GenServer import. Fixes #8. --- lib/gringotts.ex | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/gringotts.ex b/lib/gringotts.ex index d4f9e65e..11bda944 100644 --- a/lib/gringotts.ex +++ b/lib/gringotts.ex @@ -137,8 +137,6 @@ defmodule Gringotts do # some_other_key: another_value """ - import GenServer, only: [call: 2] - @doc """ Performs a (pre) Authorize operation. From 4799023796ec3a899b04f7fe6cd619b9982e4511 Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Fri, 9 Feb 2018 17:21:41 +0530 Subject: [PATCH 05/24] Refactored ResponseHandler, for new Response.t - Updated dependency `xml_builder`. The new `generate/2` provides a `format: :none | :indented` option. - `:format` is set to `:none` to produce "minified" network requests. * This is almost a complete rewrite to reduce code duplication. - check_response_type() was acting as guard that matched only against some response types. It did not handle the scenario when a non-supported response would be obtained. It really served no purpose - check_response_type -> extract_gateway_response + This guards as well as fetches, previously the fetch was being done multiple times. * Moved all response handling inside the `ResponseHandler`. * Since we now have a struct, and want to deprecate `:success`, `Response.success/1` and `Response.error/1`, helpers now act on structs. * `errorCode` and `errorText` are used to build `:reason`. + Removed pointless asserts from tests. --- lib/gringotts/gateways/authorize_net.ex | 187 ++++++++++++------------ mix.exs | 2 +- mix.lock | 2 +- test/gateways/authorize_net_test.exs | 19 +-- 4 files changed, 96 insertions(+), 114 deletions(-) diff --git a/lib/gringotts/gateways/authorize_net.ex b/lib/gringotts/gateways/authorize_net.ex index 5e2b57fd..1504eea2 100644 --- a/lib/gringotts/gateways/authorize_net.ex +++ b/lib/gringotts/gateways/authorize_net.ex @@ -101,7 +101,6 @@ defmodule Gringotts.Gateways.AuthorizeNet do """ import XmlBuilder - import XmlToMap use Gringotts.Gateways.Base use Gringotts.Adapter, required_config: [:name, :transaction_key] @@ -397,9 +396,9 @@ defmodule Gringotts.Gateways.AuthorizeNet do def store(card, opts) do request_data = if opts[:customer_profile_id] do - card |> create_customer_payment_profile(opts) |> generate + card |> create_customer_payment_profile(opts) |> generate(format: :none) else - card |> create_customer_profile(opts) |> generate + card |> create_customer_profile(opts) |> generate(format: :none) end response_data = commit(:post, request_data, opts) @@ -420,42 +419,32 @@ defmodule Gringotts.Gateways.AuthorizeNet do @spec unstore(String.t(), Keyword.t()) :: {:ok | :error, Response} def unstore(customer_profile_id, opts) do - request_data = customer_profile_id |> delete_customer_profile(opts) |> generate + request_data = customer_profile_id |> delete_customer_profile(opts) |> generate(format: :none) response_data = commit(:post, request_data, opts) respond(response_data) end - # method to make the api request with params + # method to make the API request with params defp commit(method, payload, opts) do path = base_url(opts) headers = @header HTTPoison.request(method, path, payload, headers) end - # Function to return a response - defp respond({:ok, %{body: body, status_code: 200}}) do - raw_response = naive_map(body) - response_type = ResponseHandler.check_response_type(raw_response) - response_check(raw_response[response_type], raw_response) - end + defp respond({:ok, %{body: body, status_code: 200}}), do: ResponseHandler.respond(body) defp respond({:ok, %{body: body, status_code: code}}) do - {:error, Response.error(params: body, error_code: code)} + {:error, %Response{raw: body, status_code: code}} end defp respond({:error, %HTTPoison.Error{} = error}) do - {:error, Response.error(error_code: error.id, message: "HTTPoison says '#{error.reason}'")} - end - - # Functions to send successful and error responses depending on message received - # from gateway. - - defp response_check(%{"messages" => %{"resultCode" => "Ok"}}, raw_response) do - {:ok, ResponseHandler.parse_gateway_success(raw_response)} - end - - defp response_check(%{"messages" => %{"resultCode" => "Error"}}, raw_response) do - {:error, ResponseHandler.parse_gateway_error(raw_response)} + { + :error, + %Response{ + reason: "network related failure", + message: "HTTPoison says '#{error.reason}' [ID: #{error.id || "nil"}]" + } + } end ############################################################################## @@ -470,7 +459,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do add_order_id(opts), add_purchase_transaction_request(amount, transaction_type, payment, opts) ]) - |> generate + |> generate(format: :none) end # function for formatting the request for normal capture @@ -481,7 +470,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do add_order_id(opts), add_capture_transaction_request(amount, id, transaction_type, opts) ]) - |> generate + |> generate(format: :none) end # function to format the request for normal refund @@ -492,7 +481,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do add_order_id(opts), add_refund_transaction_request(amount, id, opts, transaction_type) ]) - |> generate + |> generate(format: :none) end # function to format the request for normal void operation @@ -506,7 +495,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do add_ref_trans_id(id) ]) ]) - |> generate + |> generate(format: :none) end defp create_customer_payment_profile(card, opts) do @@ -746,88 +735,98 @@ defmodule Gringotts.Gateways.AuthorizeNet do end end + ################################################################################## + # RESPONSE_HANDLER MODULE # + # # + ################################################################################## + defmodule ResponseHandler do @moduledoc false alias Gringotts.Response - @response_type %{ - auth_response: "authenticateTestResponse", - transaction_response: "createTransactionResponse", - error_response: "ErrorResponse", - customer_profile_response: "createCustomerProfileResponse", - customer_payment_profile_response: "createCustomerPaymentProfileResponse", - delete_customer_profile: "deleteCustomerProfileResponse" - } + @supported_response_types [ + "authenticateTestResponse", + "createTransactionResponse", + "ErrorResponse", + "createCustomerProfileResponse", + "createCustomerPaymentProfileResponse", + "deleteCustomerProfileResponse" + ] + + def respond(body) do + response_map = XmlToMap.naive_map(body) + case extract_gateway_response(response_map) do + :undefined_response -> + { + :error, + %Response{ + reason: "Undefined response from AunthorizeNet", + raw: body, + message: "You might wish to open an issue with Gringotts." + } + } - def parse_gateway_success(raw_response) do - response_type = check_response_type(raw_response) - token = raw_response[response_type]["transactionResponse"]["transId"] - message = raw_response[response_type]["messages"]["message"]["text"] - avs_result = raw_response[response_type]["transactionResponse"]["avsResultCode"] - cvc_result = raw_response[response_type]["transactionResponse"]["cavvResultCode"] + result -> + build_response(result, %Response{raw: body, status_code: 200}) + end + end + + def extract_gateway_response(response_map) do + # The type of the response should be supported + @supported_response_types + |> Stream.map(&Map.get(response_map, &1, nil)) + # Find the first non-nil from the above, if all are `nil`... + # We are in trouble! + |> Enum.find(:undefined_response, &(&1)) + end - [] - |> status_code(200) - |> set_token(token) + defp build_response(%{"messages" => %{"resultCode" => "Ok"}} = result, base_response) do + {:ok, ResponseHandler.parse_gateway_success(result, base_response)} + end + + defp build_response(%{"messages" => %{"resultCode" => "Error"}} = result, base_response) do + {:error, ResponseHandler.parse_gateway_error(result, base_response)} + end + + def parse_gateway_success(result, base_response) do + id = result["transactionResponse"]["transId"] + message = result["messages"]["message"]["text"] + avs_result = result["transactionResponse"]["avsResultCode"] + cvc_result = result["transactionResponse"]["cavvResultCode"] + gateway_code = result["messages"]["message"]["code"] + + base_response + |> set_id(id) |> set_message(message) + |> set_gateway_code(gateway_code) |> set_avs_result(avs_result) |> set_cvc_result(cvc_result) - |> set_params(raw_response) - |> set_success(true) - |> handle_opts end - def parse_gateway_error(raw_response) do - response_type = check_response_type(raw_response) + def parse_gateway_error(result, base_response) do + message = result["messages"]["message"]["text"] + gateway_code = result["messages"]["message"]["code"] - {message, error_code} = - if raw_response[response_type]["transactionResponse"]["errors"] do - { - raw_response[response_type]["messages"]["message"]["text"] <> - " " <> - raw_response[response_type]["transactionResponse"]["errors"]["error"]["errorText"], - raw_response[response_type]["transactionResponse"]["errors"]["error"]["errorCode"] - } - else - { - raw_response[response_type]["messages"]["message"]["text"], - raw_response[response_type]["messages"]["message"]["code"] - } - end + error_text = result["transactionResponse"]["errors"]["error"]["errorText"] + error_code = result["transactionResponse"]["errors"]["error"]["errorCode"] + reason = "#{error_text} [Error code (#{error_code})]" - [] - |> status_code(200) + base_response |> set_message(message) - |> set_error_code(error_code) - |> set_params(raw_response) - |> set_success(false) - |> handle_opts + |> set_gateway_code(gateway_code) + |> set_reason(reason) end - def check_response_type(raw_response) do - cond do - raw_response[@response_type[:transaction_response]] -> "createTransactionResponse" - raw_response[@response_type[:error_response]] -> "ErrorResponse" - raw_response[@response_type[:customer_profile_response]] -> "createCustomerProfileResponse" - raw_response[@response_type[:customer_payment_profile_response]] -> "createCustomerPaymentProfileResponse" - raw_response[@response_type[:delete_customer_profile]] -> "deleteCustomerProfileResponse" - end - end + ############################################################################ + # HELPERS # + ############################################################################ - defp set_token(opts, token), do: [{:authorization, token} | opts] - defp set_success(opts, value), do: [{:success, value} | opts] - defp status_code(opts, code), do: [{:status, code} | opts] - defp set_message(opts, message), do: [{:message, message} | opts] - defp set_avs_result(opts, result), do: [{:avs, result} | opts] - defp set_cvc_result(opts, result), do: [{:cvc, result} | opts] - defp set_params(opts, raw_response), do: [{:params, raw_response} | opts] - defp set_error_code(opts, code), do: [{:error, code} | opts] - - defp handle_opts(opts) do - case Keyword.fetch(opts, :success) do - {:ok, true} -> Response.success(opts) - {:ok, false} -> Response.error(opts) - end - end + defp set_id(response, id), do: %{response | id: id} + defp set_message(response, message), do: %{response | message: message} + defp set_gateway_code(response, code), do: %{response | gateway_code: code} + defp set_reason(response, body), do: %{response | reason: body} + + defp set_avs_result(response, result), do: %{response | avs_result: result} + defp set_cvc_result(response, result), do: %{response | cvc_result: result} end end diff --git a/mix.exs b/mix.exs index 36d86864..f63edfc6 100644 --- a/mix.exs +++ b/mix.exs @@ -49,7 +49,7 @@ defmodule Gringotts.Mixfile do [ {:poison, "~> 3.1.0"}, {:httpoison, "~> 0.13"}, - {:xml_builder, "~> 0.1.1"}, + {:xml_builder, "~> 2.1"}, {:elixir_xml_to_map, "~> 0.1"}, # Money related diff --git a/mix.lock b/mix.lock index e3441847..da4109e3 100644 --- a/mix.lock +++ b/mix.lock @@ -35,4 +35,4 @@ "timex": {:hex, :timex, "3.1.25", "6002dae5432f749d1c93e2cd103eb73cecb53e50d2c885349e8e4146fc96bd44", [: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, "0.1.2", "b48ab9ed0a24f43a6061e0c21deda88b966a2121af5c445d4fc550dd822e23dc", [:mix], [], "hexpm"}} + "xml_builder": {:hex, :xml_builder, "2.1.0", "c249d5339427c13cae11e9d9d0e8b40d25d228b9ecc54029f24017385e60280b", [:mix], [], "hexpm"}} diff --git a/test/gateways/authorize_net_test.exs b/test/gateways/authorize_net_test.exs index b4906339..d57e3522 100644 --- a/test/gateways/authorize_net_test.exs +++ b/test/gateways/authorize_net_test.exs @@ -125,7 +125,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do MockResponse.successful_purchase_response() end do assert {:ok, response} = ANet.purchase(@amount, @card, @opts) - assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Ok" end end @@ -135,7 +134,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do MockResponse.bad_card_purchase_response() end do assert {:error, response} = ANet.purchase(@amount, @bad_card, @opts) - assert response.params["ErrorResponse"]["messages"]["resultCode"] == "Error" end end end @@ -147,7 +145,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do MockResponse.successful_authorize_response() end do assert {:ok, response} = ANet.authorize(@amount, @card, @opts) - assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Ok" end end @@ -157,7 +154,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do MockResponse.bad_card_purchase_response() end do assert {:error, response} = ANet.authorize(@amount, @bad_card, @opts) - assert response.params["ErrorResponse"]["messages"]["resultCode"] == "Error" end end end @@ -169,7 +165,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do MockResponse.successful_capture_response() end do assert {:ok, response} = ANet.capture(@capture_id, @amount, @opts) - assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Ok" end end @@ -177,7 +172,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do with_mock HTTPoison, request: fn _method, _url, _body, _headers -> MockResponse.bad_id_capture() end do assert {:error, response} = ANet.capture(@capture_invalid_id, @amount, @opts) - assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Error" end end end @@ -189,7 +183,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do MockResponse.successful_refund_response() end do assert {:ok, response} = ANet.refund(@amount, @refund_id, @opts_refund) - assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Ok" end end @@ -197,7 +190,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do with_mock HTTPoison, request: fn _method, _url, _body, _headers -> MockResponse.bad_card_refund() end do assert {:error, response} = ANet.refund(@amount, @refund_id, @opts_refund_bad_payment) - assert response.params["ErrorResponse"]["messages"]["resultCode"] == "Error" end end @@ -205,7 +197,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do with_mock HTTPoison, request: fn _method, _url, _body, _headers -> MockResponse.debit_less_than_refund() end do assert {:error, response} = ANet.refund(@amount, @refund_id, @opts_refund) - assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Error" end end end @@ -215,7 +206,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do with_mock HTTPoison, request: fn _method, _url, _body, _headers -> MockResponse.successful_void() end do assert {:ok, response} = ANet.void(@void_id, @opts) - assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Ok" end end @@ -223,7 +213,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do with_mock HTTPoison, request: fn _method, _url, _body, _headers -> MockResponse.void_non_existent_id() end do assert {:error, response} = ANet.void(@void_invalid_id, @opts) - assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Error" end end end @@ -233,7 +222,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do with_mock HTTPoison, request: fn _method, _url, _body, _headers -> MockResponse.successful_store_response() end do assert {:ok, response} = ANet.store(@card, @opts_store) - assert response.params["createCustomerProfileResponse"]["messages"]["resultCode"] == "Ok" end end @@ -241,7 +229,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do with_mock HTTPoison, request: fn _method, _url, _body, _headers -> MockResponse.successful_store_response() end do assert {:ok, response} = ANet.store(@card, @opts_store_without_validation) - assert response.params["createCustomerProfileResponse"]["messages"]["resultCode"] == "Ok" end end @@ -252,7 +239,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do end do assert {:error, response} = ANet.store(@card, @opts_store_no_profile) - assert response.params["createCustomerProfileResponse"]["messages"]["resultCode"] == "Error" end end @@ -264,7 +250,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do end do assert {:ok, response} = ANet.store(@card, @opts_customer_profile) - assert response.params["createCustomerPaymentProfileResponse"]["messages"]["resultCode"] == "Ok" end end @@ -273,7 +258,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do with_mock HTTPoison, request: fn _method, _url, _body, _headers -> MockResponse.successful_store_response() end do assert {:ok, response} = ANet.store(@card, @opts_customer_profile_args) - assert response.params["createCustomerProfileResponse"]["messages"]["resultCode"] == "Ok" end end end @@ -285,7 +269,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do MockResponse.successful_unstore_response() end do assert {:ok, response} = ANet.unstore(@unstore_id, @opts) - assert response.params["deleteCustomerProfileResponse"]["messages"]["resultCode"] == "Ok" end end end @@ -296,7 +279,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do MockResponse.netwok_error_non_existent_domain() end do assert {:error, response} = ANet.purchase(@amount, @card, @opts) - assert response.message == "HTTPoison says 'nxdomain'" + assert response.message == "HTTPoison says 'nxdomain' [ID: nil]" end end end From 470d2fe4fe4afb8da0597e2479f1409c35dbc101 Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Fri, 9 Feb 2018 17:48:04 +0530 Subject: [PATCH 06/24] Refactored `commit` and corrected avs, cvc result Also added CAVV result filed to the response which is nil for MaasterCards (as per ANet docs). --- lib/gringotts/gateways/authorize_net.ex | 109 ++++++++++++++++++------ test/gateways/authorize_net_test.exs | 36 ++++---- 2 files changed, 100 insertions(+), 45 deletions(-) diff --git a/lib/gringotts/gateways/authorize_net.ex b/lib/gringotts/gateways/authorize_net.ex index 1504eea2..b036ce52 100644 --- a/lib/gringotts/gateways/authorize_net.ex +++ b/lib/gringotts/gateways/authorize_net.ex @@ -47,11 +47,15 @@ defmodule Gringotts.Gateways.AuthorizeNet do ## Notes - Authorize.Net supports [multiple currencies][currencies] however, multiple - currencies in one account are not supported. A merchant would need multiple - Authorize.Net accounts, one for each chosen currency. - - > Currently, `Gringotts` supports single Authorize.Net account configuration. + 1. Though Authorize.Net supports [multiple currencies][currencies] however, + multiple currencies in one account are not supported in _this_ module. A + merchant would need multiple Authorize.Net accounts, one for each chosen + currency. + 2. The responses of this module include a non-standard field: `:cavv_result`. + - `:cavv_result` is the "cardholder authentication verification response + code". In case of Mastercard transactions, this field will always be + `nil`. Please refer the "Response Format" section in the [docs][docs] for + more details. [currencies]: https://community.developer.authorize.net/t5/The-Authorize-Net-Developer-Blog/Authorize-Net-UK-Europe-Update/ba-p/35957 @@ -108,7 +112,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do @test_url "https://apitest.authorize.net/xml/v1/request.api" @production_url "https://api.authorize.net/xml/v1/request.api" - @header [{"Content-Type", "text/xml"}] + @headers [{"Content-Type", "text/xml"}] @transaction_type %{ purchase: "authCaptureTransaction", @@ -181,8 +185,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do @spec purchase(Money.t(), CreditCard.t(), Keyword.t()) :: {:ok | :error, Response} def purchase(amount, payment, opts) do request_data = add_auth_purchase(amount, payment, opts, @transaction_type[:purchase]) - response_data = commit(:post, request_data, opts) - respond(response_data) + commit(request_data, opts) end @doc """ @@ -242,8 +245,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do @spec authorize(Money.t(), CreditCard.t(), Keyword.t()) :: {:ok | :error, Response} def authorize(amount, payment, opts) do request_data = add_auth_purchase(amount, payment, opts, @transaction_type[:authorize]) - response_data = commit(:post, request_data, opts) - respond(response_data) + commit(request_data, opts) end @doc """ @@ -285,8 +287,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do @spec capture(String.t(), Money.t(), Keyword.t()) :: {:ok | :error, Response} def capture(id, amount, opts) do request_data = normal_capture(amount, id, opts, @transaction_type[:capture]) - response_data = commit(:post, request_data, opts) - respond(response_data) + commit(request_data, opts) end @doc """ @@ -315,8 +316,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do @spec refund(Money.t(), String.t(), Keyword.t()) :: {:ok | :error, Response} def refund(amount, id, opts) do request_data = normal_refund(amount, id, opts, @transaction_type[:refund]) - response_data = commit(:post, request_data, opts) - respond(response_data) + commit(request_data, opts) end @doc """ @@ -341,8 +341,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do @spec void(String.t(), Keyword.t()) :: {:ok | :error, Response} def void(id, opts) do request_data = normal_void(id, opts, @transaction_type[:void]) - response_data = commit(:post, request_data, opts) - respond(response_data) + commit(request_data, opts) end @doc """ @@ -401,8 +400,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do card |> create_customer_profile(opts) |> generate(format: :none) end - response_data = commit(:post, request_data, opts) - respond(response_data) + commit(request_data, opts) end @doc """ @@ -420,15 +418,15 @@ defmodule Gringotts.Gateways.AuthorizeNet do @spec unstore(String.t(), Keyword.t()) :: {:ok | :error, Response} def unstore(customer_profile_id, opts) do request_data = customer_profile_id |> delete_customer_profile(opts) |> generate(format: :none) - response_data = commit(:post, request_data, opts) - respond(response_data) + commit(request_data, opts) end # method to make the API request with params - defp commit(method, payload, opts) do - path = base_url(opts) - headers = @header - HTTPoison.request(method, path, payload, headers) + defp commit(payload, opts) do + opts + |> base_url() + |> HTTPoison.post(payload, @headers) + |> respond() end defp respond({:ok, %{body: body, status_code: 200}}), do: ResponseHandler.respond(body) @@ -753,6 +751,50 @@ defmodule Gringotts.Gateways.AuthorizeNet do "deleteCustomerProfileResponse" ] + @avs_code_translator %{ + "A" => {"pass", "fail"}, #The street address matched, but the postal code did not. + "B" => {nil, nil}, # No address information was provided. + "E" => {"fail", nil}, # The AVS check returned an error. + "G" => {nil, nil}, # The card was issued by a bank outside the U.S. and does not support AVS. + "N" => {"fail", "fail"}, # Neither the street address nor postal code matched. + "P" => {nil, nil}, # AVS is not applicable for this transaction. + "R" => {nil, nil}, # Retry — AVS was unavailable or timed out. + "S" => {nil, nil}, # AVS is not supported by card issuer. + "U" => {nil, nil}, # Address information is unavailable. + "W" => {"fail", "pass"}, # The US ZIP+4 code matches, but the street address does not. + "X" => {"pass", "pass"}, # Both the street address and the US ZIP+4 code matched. + "Y" => {"pass", "pass"}, # The street address and postal code matched. + "Z" => {"fail", "pass"}, # The postal code matched, but the street address did not. + "" => {nil, nil}, # fallback in-case of absence + nil => {nil, nil} # fallback in-case of absence + } + + @cvc_code_translator %{ + "M" => "CVV matched.", + "N" => "CVV did not match.", + "P" => "CVV was not processed.", + "S" => "CVV should have been present but was not indicated.", + "U" => "The issuer was unable to process the CVV check.", + nil => nil # fallback in-case of absence + } + + @cavv_code_translator %{ + "" => "CAVV not validated.", + "0" => "CAVV was not validated because erroneous data was submitted.", + "1" => "CAVV failed validation.", + "2" => "CAVV passed validation.", + "3" => "CAVV validation could not be performed; issuer attempt incomplete.", + "4" => "CAVV validation could not be performed; issuer system error.", + "5" => "Reserved for future use.", + "6" => "Reserved for future use.", + "7" => "CAVV failed validation, but the issuer is available. Valid for U.S.-issued card submitted to non-U.S acquirer.", + "8" => "CAVV passed validation and the issuer is available. Valid for U.S.-issued card submitted to non-U.S. acquirer.", + "9" => "CAVV failed validation and the issuer is unavailable. Valid for U.S.-issued card submitted to non-U.S acquirer.", + "A" => "CAVV passed validation but the issuer unavailable. Valid for U.S.-issued card submitted to non-U.S acquirer.", + "B" => "CAVV passed validation, information only, no liability shift.", + nil => nil # fallback in-case of absence + } + def respond(body) do response_map = XmlToMap.naive_map(body) case extract_gateway_response(response_map) do @@ -792,7 +834,8 @@ defmodule Gringotts.Gateways.AuthorizeNet do id = result["transactionResponse"]["transId"] message = result["messages"]["message"]["text"] avs_result = result["transactionResponse"]["avsResultCode"] - cvc_result = result["transactionResponse"]["cavvResultCode"] + cvc_result = result["transactionResponse"]["cvvResultCode"] + cavv_result = result["transactionResponse"]["cavvResultCode"] gateway_code = result["messages"]["message"]["code"] base_response @@ -801,6 +844,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do |> set_gateway_code(gateway_code) |> set_avs_result(avs_result) |> set_cvc_result(cvc_result) + |> set_cavv_result(cavv_result) end def parse_gateway_error(result, base_response) do @@ -826,7 +870,18 @@ defmodule Gringotts.Gateways.AuthorizeNet do defp set_gateway_code(response, code), do: %{response | gateway_code: code} defp set_reason(response, body), do: %{response | reason: body} - defp set_avs_result(response, result), do: %{response | avs_result: result} - defp set_cvc_result(response, result), do: %{response | cvc_result: result} + defp set_avs_result(response, avs_code) do + {street, zip_code} = @avs_code_translator[avs_code] + %{response | avs_result: %{street: street, zip_code: zip_code}} + end + + defp set_cvc_result(response, cvv_code) do + %{response | cvc_result: @cvc_code_translator[cvv_code]} + end + + defp set_cavv_result(response, cavv_code) do + Map.put(response, :cavv_result, @cavv_code_translator[cavv_code]) + end + end end diff --git a/test/gateways/authorize_net_test.exs b/test/gateways/authorize_net_test.exs index d57e3522..2ce52278 100644 --- a/test/gateways/authorize_net_test.exs +++ b/test/gateways/authorize_net_test.exs @@ -121,7 +121,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do describe "purchase" do test "successful response with right params" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> + post: fn _url, _body, _headers -> MockResponse.successful_purchase_response() end do assert {:ok, response} = ANet.purchase(@amount, @card, @opts) @@ -130,7 +130,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do test "with bad card" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> + post: fn _url, _body, _headers -> MockResponse.bad_card_purchase_response() end do assert {:error, response} = ANet.purchase(@amount, @bad_card, @opts) @@ -141,7 +141,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do describe "authorize" do test "successful response with right params" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> + post: fn _url, _body, _headers -> MockResponse.successful_authorize_response() end do assert {:ok, response} = ANet.authorize(@amount, @card, @opts) @@ -150,7 +150,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do test "with bad card" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> + post: fn _url, _body, _headers -> MockResponse.bad_card_purchase_response() end do assert {:error, response} = ANet.authorize(@amount, @bad_card, @opts) @@ -161,7 +161,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do describe "capture" do test "successful response with right params" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> + post: fn _url, _body, _headers -> MockResponse.successful_capture_response() end do assert {:ok, response} = ANet.capture(@capture_id, @amount, @opts) @@ -170,7 +170,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do test "with bad transaction id" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> MockResponse.bad_id_capture() end do + post: fn _url, _body, _headers -> MockResponse.bad_id_capture() end do assert {:error, response} = ANet.capture(@capture_invalid_id, @amount, @opts) end end @@ -179,7 +179,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do describe "refund" do test "successful response with right params" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> + post: fn _url, _body, _headers -> MockResponse.successful_refund_response() end do assert {:ok, response} = ANet.refund(@amount, @refund_id, @opts_refund) @@ -188,14 +188,14 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do test "bad payment params" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> MockResponse.bad_card_refund() end do + post: fn _url, _body, _headers -> MockResponse.bad_card_refund() end do assert {:error, response} = ANet.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 -> MockResponse.debit_less_than_refund() end do + post: fn _url, _body, _headers -> MockResponse.debit_less_than_refund() end do assert {:error, response} = ANet.refund(@amount, @refund_id, @opts_refund) end end @@ -204,14 +204,14 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do describe "void" do test "successful response with right params" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> MockResponse.successful_void() end do + post: fn _url, _body, _headers -> MockResponse.successful_void() end do assert {:ok, response} = ANet.void(@void_id, @opts) end end test "with bad transaction id" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> MockResponse.void_non_existent_id() end do + post: fn _url, _body, _headers -> MockResponse.void_non_existent_id() end do assert {:error, response} = ANet.void(@void_invalid_id, @opts) end end @@ -220,21 +220,21 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do describe "store" do test "successful response with right params" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> MockResponse.successful_store_response() end do + post: fn _url, _body, _headers -> MockResponse.successful_store_response() end do assert {:ok, response} = ANet.store(@card, @opts_store) end end test "successful response without validation and customer type" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> MockResponse.successful_store_response() end do + post: fn _url, _body, _headers -> MockResponse.successful_store_response() end do assert {:ok, response} = ANet.store(@card, @opts_store_without_validation) end end test "without any profile" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> + post: fn _url, _body, _headers -> MockResponse.store_without_profile_fields() end do assert {:error, response} = ANet.store(@card, @opts_store_no_profile) @@ -245,7 +245,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do test "with customer profile id" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> + post: fn _url, _body, _headers -> MockResponse.customer_payment_profile_success_response() end do assert {:ok, response} = ANet.store(@card, @opts_customer_profile) @@ -256,7 +256,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do test "successful response without valiadtion mode and customer type" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> MockResponse.successful_store_response() end do + post: fn _url, _body, _headers -> MockResponse.successful_store_response() end do assert {:ok, response} = ANet.store(@card, @opts_customer_profile_args) end end @@ -265,7 +265,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do describe "unstore" do test "successful response with right params" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> + post: fn _url, _body, _headers -> MockResponse.successful_unstore_response() end do assert {:ok, response} = ANet.unstore(@unstore_id, @opts) @@ -275,7 +275,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do test "network error type non existent domain" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> + post: fn _url, _body, _headers -> MockResponse.netwok_error_non_existent_domain() end do assert {:error, response} = ANet.purchase(@amount, @card, @opts) From 3040499dffc9e218906928562dbeb2f1480d01ff Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Fri, 9 Feb 2018 21:59:32 +0530 Subject: [PATCH 07/24] Correct a few doc examples --- lib/gringotts/gateways/authorize_net.ex | 39 ++++++++++++------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/lib/gringotts/gateways/authorize_net.ex b/lib/gringotts/gateways/authorize_net.ex index b036ce52..14950eb9 100644 --- a/lib/gringotts/gateways/authorize_net.ex +++ b/lib/gringotts/gateways/authorize_net.ex @@ -94,7 +94,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do ``` iex> alias Gringotts.{Response, CreditCard, Gateways.AuthorizeNet} - iex> amount = %{value: Decimal.new(20.0), currency: "USD"} + iex> amount = Money.new(20, :USD} iex> card = %CreditCard{number: "5424000000000015", year: 2099, month: 12, verification_code: "999"} ``` @@ -136,8 +136,8 @@ defmodule Gringotts.Gateways.AuthorizeNet do Charges a credit `card` for the specified `amount`. It performs `authorize` and `capture` at the [same time][auth-cap-same-time]. - Authorize.Net returns `transId` (available in the `Response.authorization` - field) which can be used to: + Authorize.Net returns `transId` (available in the `Response.id` field) which + can be used to: * `refund/3` a settled transaction. * `void/2` a transaction. @@ -170,7 +170,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do ] ## Example - iex> amount = %{value: Decimal.new(20.0), currency: "USD"} + iex> amount = Money.new(20, :USD} iex> opts = [ ref_id: "123456", order: %{invoice_number: "INV-12345", description: "Product Description"}, @@ -182,7 +182,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do iex> card = %CreditCard{number: "5424000000000015", year: 2099, month: 12, verification_code: "999"} iex> result = Gringotts.purchase(Gringotts.Gateways.AuthorizeNet, amount, card, opts) """ - @spec purchase(Money.t(), CreditCard.t(), Keyword.t()) :: {:ok | :error, Response} + @spec purchase(Money.t(), CreditCard.t(), Keyword.t()) :: {:ok | :error, Response.t()} def purchase(amount, payment, opts) do request_data = add_auth_purchase(amount, payment, opts, @transaction_type[:purchase]) commit(request_data, opts) @@ -197,8 +197,8 @@ defmodule Gringotts.Gateways.AuthorizeNet do To transfer the funds to merchant's account follow this up with a `capture/3`. - Authorize.Net returns a `transId` (available in the `Response.authorization` - field) which can be used for: + Authorize.Net returns a `transId` (available in the `Response.id` field) which + can be used for: * `capture/3` an authorized transaction. * `void/2` a transaction. @@ -230,7 +230,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do ## Example - iex> amount = %{value: Decimal.new(20.0), currency: "USD"} + iex> amount = Money.new(20, :USD} iex> opts = [ ref_id: "123456", order: %{invoice_number: "INV-12345", description: "Product Description"}, @@ -242,7 +242,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do iex> card = %CreditCard{number: "5424000000000015", year: 2099, month: 12, verification_code: "999"} iex> result = Gringotts.authorize(Gringotts.Gateways.AuthorizeNet, amount, card, opts) """ - @spec authorize(Money.t(), CreditCard.t(), Keyword.t()) :: {:ok | :error, Response} + @spec authorize(Money.t(), CreditCard.t(), Keyword.t()) :: {:ok | :error, Response.t()} def authorize(amount, payment, opts) do request_data = add_auth_purchase(amount, payment, opts, @transaction_type[:authorize]) commit(request_data, opts) @@ -254,8 +254,8 @@ defmodule Gringotts.Gateways.AuthorizeNet do `amount` is transferred to the merchant account by Authorize.Net when it is smaller or equal to the amount used in the pre-authorization referenced by `id`. - Authorize.Net returns a `transId` (available in the `Response.authorization` - field) which can be used to: + Authorize.Net returns a `transId` (available in the `Response.id` field) which + can be used to: * `refund/3` a settled transaction. * `void/2` a transaction. @@ -280,11 +280,11 @@ defmodule Gringotts.Gateways.AuthorizeNet do iex> opts = [ ref_id: "123456" ] - iex> amount = %{value: Decimal.new(20.0), currency: "USD"} + iex> amount = Money.new(20, :USD} iex> id = "123456" iex> result = Gringotts.capture(Gringotts.Gateways.AuthorizeNet, id, amount, opts) """ - @spec capture(String.t(), Money.t(), Keyword.t()) :: {:ok | :error, Response} + @spec capture(String.t(), Money.t(), Keyword.t()) :: {:ok | :error, Response.t()} def capture(id, amount, opts) do request_data = normal_capture(amount, id, opts, @transaction_type[:capture]) commit(request_data, opts) @@ -310,10 +310,10 @@ defmodule Gringotts.Gateways.AuthorizeNet do ref_id: "123456" ] iex> id = "123456" - iex> amount = %{value: Decimal.new(20.0), currency: "USD"} + iex> amount = Money.new(20, :USD} iex> result = Gringotts.refund(Gringotts.Gateways.AuthorizeNet, amount, id, opts) """ - @spec refund(Money.t(), String.t(), Keyword.t()) :: {:ok | :error, Response} + @spec refund(Money.t(), String.t(), Keyword.t()) :: {:ok | :error, Response.t()} def refund(amount, id, opts) do request_data = normal_refund(amount, id, opts, @transaction_type[:refund]) commit(request_data, opts) @@ -338,7 +338,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do iex> id = "123456" iex> result = Gringotts.void(Gringotts.Gateways.AuthorizeNet, id, opts) """ - @spec void(String.t(), Keyword.t()) :: {:ok | :error, Response} + @spec void(String.t(), Keyword.t()) :: {:ok | :error, Response.t()} def void(id, opts) do request_data = normal_void(id, opts, @transaction_type[:void]) commit(request_data, opts) @@ -391,7 +391,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do iex> card = %CreditCard{number: "5424000000000015", year: 2099, month: 12, verification_code: "999"} iex> result = Gringotts.store(Gringotts.Gateways.AuthorizeNet, card, opts) """ - @spec store(CreditCard.t(), Keyword.t()) :: {:ok | :error, Response} + @spec store(CreditCard.t(), Keyword.t()) :: {:ok | :error, Response.t()} def store(card, opts) do request_data = if opts[:customer_profile_id] do @@ -411,11 +411,10 @@ defmodule Gringotts.Gateways.AuthorizeNet do ## Example iex> id = "123456" - iex> opts = [] - iex> result = Gringotts.store(Gringotts.Gateways.AuthorizeNet, id, opts) + iex> result = Gringotts.store(Gringotts.Gateways.AuthorizeNet, id) """ - @spec unstore(String.t(), Keyword.t()) :: {:ok | :error, Response} + @spec unstore(String.t(), Keyword.t()) :: {:ok | :error, Response.t()} def unstore(customer_profile_id, opts) do request_data = customer_profile_id |> delete_customer_profile(opts) |> generate(format: :none) commit(request_data, opts) From 256072fec204aa248ce75c9dc58889dc9d482f99 Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Thu, 15 Mar 2018 20:58:47 +0530 Subject: [PATCH 08/24] Adds changelog, contributing guide and improves mix task (#117) * Fix Adapter moduledoc and bogus gateway * Fixes #24 * bogus test also uses Money protocol now * Changed validate_config docs * Improved mix task docs (filename) * Better module name suggestion * Now, Filename can be specified on the CLI with the `-f` flag * Added changelog and contributing guide. * Also reworded README slightly * Correct "amount" in examples to ex_money * Replaced example bindings with links to `.iex.exs` * Removed unused params from functions. * Fix call to `downcase` --- CHANGELOG.md | 52 ++++++++++++++ CONTRIBUTING.md | 91 +++++++++++++++++++++++++ README.md | 58 ++++++++++++---- lib/gringotts/adapter.ex | 49 ++++++++++--- lib/gringotts/gateways/authorize_net.ex | 32 ++++----- lib/gringotts/gateways/bogus.ex | 33 ++++----- lib/gringotts/gateways/cams.ex | 37 +++++----- lib/gringotts/gateways/monei.ex | 63 +++-------------- lib/gringotts/gateways/trexle.ex | 41 +++-------- lib/mix/new.ex | 61 +++++++++++------ mix.exs | 6 +- mix.lock | 12 ++-- templates/mock_response.eex | 2 +- templates/test.eex | 4 +- test/gateways/bogus_test.exs | 13 ++-- 15 files changed, 358 insertions(+), 196 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..5a1f16ad --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,52 @@ +# [`v1.1.0-alpha`][tag-1_1_0_alpha] + +## Added + +* [`ISS`][iss#80] [`PR`][pr#78] +Add a `Mix` task that generates a barebones gateway implementation and test suite. + +## Changed + +* [`ISS`][iss#62] [`PR`][pr#71] [`PR`][pr#86] +Deprecate use of `floats` for money amounts, introduce the `Gringotts.Money` protocol. + +[iss#62]: https://github.com/aviabird/gringotts/issues/62 +[iss#80]: https://github.com/aviabird/gringotts/issues/80 + +[pr#71]: https://github.com/aviabird/gringotts/pulls/71 +[pr#78]:https://github.com/aviabird/gringotts/pulls/78 +[pr#86]:https://github.com/aviabird/gringotts/pulls/86 + +# [`v1.0.2`][tag-1_0_2] + +## Added + +* New Gateway: **Trexle** + +## Changed + +* Reduced arity of public API calls by 1 + - No need to pass the name of the `worker` as argument. + +# [`v1.0.1`][tag-1_0_1] + +## Added + +* Improved documentation - made consistent accross gateways +* Improved test coverage + +# [`v1.0.0`][tag-1_0_0] + +* Initial public API release. +* Single worker architecture, config fetched from `config.exs` +* Supported Gateways: + - Stripe + - MONEI + - Paymill + - WireCard + - CAMS + +[tag-1_1_0_alpha]: https://github.com/aviabird/gringotts/releases/tag/v1.1.0-alpha +[tag-1_0_2]: https://github.com/aviabird/gringotts/releases/tag/v1.0.2 +[tag-1_0_1]: https://github.com/aviabird/gringotts/releases/tag/v1.0.1 +[tag-1_0_0]: https://github.com/aviabird/gringotts/releases/tag/v1.0.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..6eeb7033 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,91 @@ +# Contributing to [`gringotts`][gringotts] + +There are many ways to contribute to `gringotts`, + +1. [Integrate a new Payment Gateway][wiki-new-gateway]. +2. Expanding the feature coverage of (partially) supported gateways. +3. Moving forward on the [roadmap][roadmap] or on tasks being tracked in the + [milestones][milestones]. + +We manage our development using [milestones][milestones] and issues so if you're +a first time contributor, look out for the [`good first issue`][first-issues] +and the [`hotlist: community-help`][ch-issues] labels on the [issues][issues] +page. + +The docs are hosted on [hexdocs.pm][hexdocs] and are updated for each +release. **You must build the docs locally using `mix docs` to get the bleeding +edge developer docs.** + +The article on [Gringott's Architecture][wiki-arch] explains how API calls are +processed. + +:exclamation: ***Please base your work on the `dev` branch.*** + +[roadmap]: https://github.com/aviabird/gringotts/wiki/Roadmap +[wiki-arch]: https://github.com/aviabird/gringotts/wiki/Architecture + +### PR submission checklist + +Each PR should introduce a *focussed set of changes*, and ideally not span over +unrelated modules. + +* [ ] Run the edited files through [credo][credo] and the Elixir + [formatter][hashrocket-formatter] (new in `v1.6`). +* [ ] Check the test coverage by running `mix coveralls`. 100% coverage is not + strictly required. +* [ ] If the PR introduces a new Gateway or just Gateway specific changes, + please format the title like so,\ + `[] ` + +[gringotts]: https://github.com/aviabird/gringotts +[milestones]: https://github.com/aviabird/gringotts/milestones +[issues]: https://github.com/aviabird/gringotts/issues +[first-issues]: https://github.com/aviabird/gringotts/issues?q=is%3Aissue+is%3Aopen+label%3A"good+first+issue" +[ch-issues]: https://github.com/aviabird/gringotts/issues?q=is%3Aissue+is%3Aopen+label%3A"hotfix%3A+community-help" +[hexdocs]: https://hexdocs.pm/gringotts +[credo]: https://github.com/rrrene/credo +[hashrocket-formatter]: https://hashrocket.com/blog/posts/format-your-elixir-code-now + +# Style Guidelines + +We use [`credo`][credo] and the elixir formatter for consistent style, so please +use them! + +## General Rules + +* Keep line length below 120 characters. +* Complex anonymous functions should be extracted into named functions. +* One line functions, should only take up one line! +* Pipes are great, but don't use them, if they are less readable than brackets + then drop the pipe! + +## Writing documentation + +All our docs are inline and built using [`ExDocs`][exdocs]. Please take a look +at how the docs are structured for the [MONEI gateway][src-monei] for +inspiration. + +[exdocs]: https://github.com/elixir-lang/ex_doc +[src-monei]: https://github.com/aviabird/gringotts/blob/dev/lib/gringotts/gateways/monei.ex + +## Writing test cases + +> This is WIP. + +`gringotts` has mock and integration tests. We have currently used +[`bypass`][bypass] and [`mock`][mock] for mock tests, but we don't recommed +using `mock` as it constrains tests to run serially. Use [`mox`][mox] instead.\ +Take a look at [MONEI's mock tests][src-monei-tests] for inspiration. + +-------------------------------------------------------------------------------- + +> **Where to next?** +> Wanna add a new gateway? Head to our [guide][wiki-new-gateway] for that. + +[wiki-new-gateway]: https://github.com/aviabird/gringotts/wiki/Adding-a-new-Gateway +[bypass]: https://github.com/pspdfkit-labs/bypass +[mock]: https://github.com/jjh42/mock +[mox]: https://github.com/plataformatec/mox +[src-monei-tests]: https://github.com/aviabird/gringotts/blob/dev/test/gateways/monei_test.exs +[gringotts]: https://github.com/aviabird/gringotts +[docs]: https://hexdocs.pm/gringotts/Gringotts.html diff --git a/README.md b/README.md index 6b71deb4..ce98c205 100644 --- a/README.md +++ b/README.md @@ -5,24 +5,24 @@

- Gringotts is a payment processing library in Elixir integrating various payment gateways, this draws motivation for shopify's activemerchant gem. Checkout the Demo here. + Gringotts is a payment processing library in Elixir integrating various payment gateways, drawing motivation for Shopify's activemerchant gem. Checkout the Demo here.

Build Status Coverage Status Docs coverage Help Contribute to Open Source

-A simple and unified API to access dozens of different payment -gateways with very different internal APIs is what Gringotts has to offer you. +Gringotts offers a **simple and unified API** to access dozens of different payment +gateways with very different APIs, response schemas, documentation and jargon. ## Installation -### From hex.pm +### From [`hex.pm`][hexpm] -Make the following changes to the `mix.exs` file. - -Add gringotts to the list of dependencies. +Add `gringotts` to the list of dependencies of your application. ```elixir +# your mix.exs + def deps do [ {:gringotts, "~> 1.0"}, @@ -35,23 +35,31 @@ end ## Usage -This simple example demonstrates how a purchase can be made using a person's credit card details. +This simple example demonstrates how a `purchase` can be made using a sample +credit card using the [MONEI][monei] gateway. -Add configs in `config/config.exs` file. +One must "register" their account with `gringotts` ie, put all the +authentication details in the Application config. Usually via +`config/config.exs` ```elixir +# config/config.exs + config :gringotts, Gringotts.Gateways.Monei, userId: "your_secret_user_id", password: "your_secret_password", entityId: "your_secret_channel_id" ``` -Copy and paste this code in your module +Copy and paste this code in a module or an `IEx` session ```elixir alias Gringotts.Gateways.Monei alias Gringotts.{CreditCard} +# a fake sample card that will work now because the Gateway is by default +# in "test" mode. + card = %CreditCard{ first_name: "Harry", last_name: "Potter", @@ -61,9 +69,10 @@ card = %CreditCard{ brand: "VISA" } +# a sum of $42 amount = Money.new(42, :USD) -case Gringotts.purchase(Monei, amount, card, opts) do +case Gringotts.purchase(Monei, amount, card) do {:ok, %{id: id}} -> IO.puts("Payment authorized, reference token: '#{id}'") @@ -72,6 +81,9 @@ case Gringotts.purchase(Monei, amount, card, opts) do end ``` +[hexpm]: https://hex.pm/packages/gringotts +[monei]: http://www.monei.net + ## Supported Gateways | Gateway | Supported countries | @@ -95,9 +107,29 @@ end ## Road Map -- Support more gateways on an on-going basis. -- Each gateway request is hosted in a worker process and supervised. +Apart from supporting more and more gateways, we also keep a somewhat detailed +plan for the future on our [wiki][roadmap]. + +## FAQ + +#### 1. What's with the name? "Gringotts"? + +Gringotts has a nice ring to it. Also [this][reason]. + +#### 2. What is the worker doing in the middle? + +We wanted to "supervise" our payments, and power utilities to process recurring +payments, subscriptions with it. But yes, as of now, it is a bottle neck and +unnecessary. + +It's slated to be removed in [`v2.0.0`][milestone-2_0_0_alpha] and any supervised / async / +parallel work can be explicitly managed via native elixir constructs. + +[milestone-2_0_0_alpha]: https://github.com/aviabird/gringotts/milestone/3 +[reason]: http://harrypotter.wikia.com/wiki/Gringotts ## License MIT + +[roadmap]: https://github.com/aviabird/gringotts/wiki/Roadmap diff --git a/lib/gringotts/adapter.ex b/lib/gringotts/adapter.ex index 8d2a24f8..978cd1d1 100644 --- a/lib/gringotts/adapter.ex +++ b/lib/gringotts/adapter.ex @@ -1,20 +1,53 @@ defmodule Gringotts.Adapter do - @moduledoc ~S""" - Adapter module is currently holding the validation part. + @moduledoc """ + Validates the "required" configuration. - This modules is being `used` by all the payment gateways and raises a run-time - error for the missing configurations which are passed by the gateways to - `validate_config` method. + All gateway modules must `use` this module, which provides a run-time + configuration validator. - Raises an exception `ArgumentError` if the config is not as per the `@required_config` - """ + Gringotts picks up the merchant's Gateway authentication secrets from the + Application config. The configuration validator can be customized by providing + a list of `required_config` keys. The validator will check if these keys are + available at run-time, before each call to the Gateway. + + ## Example + + Say a merchant must provide his `secret_user_name` and `secret_password` to + some Gateway `XYZ`. Then, `Gringotts` expects that the `GatewayXYZ` module + would use `Adapter` in the following manner: + + ``` + defmodule Gringotts.Gateways.GatewayXYZ do + + use Gringotts.Adapter, required_config: [:secret_user_name, :secret_password] + use Gringotts.Gateways.Base + + # the rest of the implentation + end + ``` + And, the merchant woud provide these secrets in the Application config, + possibly via `config/config.exs` like so, + ``` + # config/config.exs + + config :gringotts, Gringotts.Gateways.GatewayXYZ, + adapter: Gringotts.Gateways.GatewayXYZ, + secret_user_name: "some_really_secret_user_name", + secret_password: "some_really_secret_password" + + ``` + """ + defmacro __using__(opts) do quote bind_quoted: [opts: opts] do @required_config opts[:required_config] || [] @doc """ - Validates the config dynamically depending on what is the value of `required_config` + Catches gateway configuration errors. + + Raises a run-time `ArgumentError` if any of the `required_config` values + is not available or missing from the Application config. """ def validate_config(config) do missing_keys = Enum.reduce(@required_config, [], fn(key, missing_keys) -> diff --git a/lib/gringotts/gateways/authorize_net.ex b/lib/gringotts/gateways/authorize_net.ex index 14950eb9..59281314 100644 --- a/lib/gringotts/gateways/authorize_net.ex +++ b/lib/gringotts/gateways/authorize_net.ex @@ -89,18 +89,16 @@ defmodule Gringotts.Gateways.AuthorizeNet do that gives you a pre-configured sample app ready-to-go. + You could use the same config or update it the with your "secrets" [above](#module-configuring-your-authorizenet-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): + + 2. To save a lot of time, create a [`.iex.exs`][iex-docs] file as shown in + [this gist][authorize_net.iex.exs] to introduce a set of handy bindings and + aliases. - ``` - iex> alias Gringotts.{Response, CreditCard, Gateways.AuthorizeNet} - iex> amount = Money.new(20, :USD} - iex> card = %CreditCard{number: "5424000000000015", year: 2099, month: 12, verification_code: "999"} - ``` - - We'll be using these in the examples below. + We'll be using these bindings in the examples below. [example-repo]: https://github.com/aviabird/gringotts_example + [iex-docs]: https://hexdocs.pm/iex/IEx.html#module-the-iex-exs-file + [authorize_net.iex.exs]: https://gist.github.com/oyeb/b1030058bda1fa9a3d81f1cf30723695 [gs]: https://github.com/aviabird/gringotts/wiki """ @@ -170,7 +168,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do ] ## Example - iex> amount = Money.new(20, :USD} + iex> amount = Money.new(20, :USD) iex> opts = [ ref_id: "123456", order: %{invoice_number: "INV-12345", description: "Product Description"}, @@ -230,7 +228,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do ## Example - iex> amount = Money.new(20, :USD} + iex> amount = Money.new(20, :USD) iex> opts = [ ref_id: "123456", order: %{invoice_number: "INV-12345", description: "Product Description"}, @@ -280,7 +278,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do iex> opts = [ ref_id: "123456" ] - iex> amount = Money.new(20, :USD} + iex> amount = Money.new(20, :USD) iex> id = "123456" iex> result = Gringotts.capture(Gringotts.Gateways.AuthorizeNet, id, amount, opts) """ @@ -310,7 +308,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do ref_id: "123456" ] iex> id = "123456" - iex> amount = Money.new(20, :USD} + iex> amount = Money.new(20, :USD) iex> result = Gringotts.refund(Gringotts.Gateways.AuthorizeNet, amount, id, opts) """ @spec refund(Money.t(), String.t(), Keyword.t()) :: {:ok | :error, Response.t()} @@ -465,7 +463,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do |> element(%{xmlns: @aut_net_namespace}, [ add_merchant_auth(opts[:config]), add_order_id(opts), - add_capture_transaction_request(amount, id, transaction_type, opts) + add_capture_transaction_request(amount, id, transaction_type) ]) |> generate(format: :none) end @@ -561,7 +559,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do add_transaction_type(transaction_type), add_amount(amount), add_payment_source(payment), - add_invoice(transaction_type, opts), + add_invoice(opts), add_tax_fields(opts), add_duty_fields(opts), add_shipping_fields(opts), @@ -570,7 +568,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do ]) end - defp add_capture_transaction_request(amount, id, transaction_type, opts) do + defp add_capture_transaction_request(amount, id, transaction_type) do element(:transactionRequest, [ add_transaction_type(transaction_type), add_amount(amount), @@ -626,7 +624,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do ]) end - defp add_invoice(transactionType, opts) do + defp add_invoice(opts) do element([ element(:order, [ element(:invoiceNumber, opts[:order][:invoice_number]), diff --git a/lib/gringotts/gateways/bogus.ex b/lib/gringotts/gateways/bogus.ex index de903744..d6bdaa02 100644 --- a/lib/gringotts/gateways/bogus.ex +++ b/lib/gringotts/gateways/bogus.ex @@ -1,4 +1,6 @@ defmodule Gringotts.Gateways.Bogus do + @moduledoc false + use Gringotts.Gateways.Base alias Gringotts.{ @@ -6,36 +8,29 @@ defmodule Gringotts.Gateways.Bogus do Response } + @some_authorization_id "14a62fff80f24a25f775eeb33624bbb3" + def authorize(_amount, _card_or_id, _opts), do: success() def purchase(_amount, _card_or_id, _opts), do: success() - def capture(id, amount, _opts), - do: success(id) + def capture(_id, _amount, _opts), + do: success() - def void(id, _opts), - do: success(id) + def void(_id, _opts), + do: success() - def refund(_amount, id, _opts), - do: success(id) + def refund(_amount, _id, _opts), + do: success() - def store(_card = %CreditCard{}, _opts), + def store(%CreditCard{} = _card, _opts), do: success() - def unstore(customer_id, _opts), - do: success(customer_id) + def unstore(_customer_id, _opts), + do: success() defp success, - do: {:ok, Response.success(id: random_string())} - - defp success(id), - do: {:ok, Response.success(id: id)} - - defp random_string(length \\ 10), - do: 1..length |> Enum.map(&random_char/1) |> Enum.join - - defp random_char(_), - do: to_string(:rand.uniform(9)) + do: {:ok, Response.success(id: @some_authorization_id)} end diff --git a/lib/gringotts/gateways/cams.ex b/lib/gringotts/gateways/cams.ex index 1787e947..0cb0c4e8 100644 --- a/lib/gringotts/gateways/cams.ex +++ b/lib/gringotts/gateways/cams.ex @@ -38,13 +38,13 @@ defmodule Gringotts.Gateways.Cams do this is important to you. [issues]: https://github.com/aviabird/gringotts/issues/new - + ### Schema * `billing_address` is a `map` from `atoms` to `String.t`, and can include any of the keys from: `:name, :address1, :address2, :company, :city, :state, :zip, :country, :phone, :fax]` - + ## Registering your CAMS account at `Gringotts` | Config parameter | CAMS secret | @@ -81,26 +81,21 @@ defmodule Gringotts.Gateways.Cams do you get after [registering with CAMS](#module-registering-your-cams-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): - ``` - iex> alias Gringotts.{Response, CreditCard, Gateways.Cams} - iex> card = %CreditCard{first_name: "Harry", - last_name: "Potter", - number: "4111111111111111", - year: 2099, - month: 12, - verification_code: "999", - brand: "VISA"} - iex> money = %{value: Decimal.new(20), currency: "USD"} - ``` - We'll be using these in the examples below. + 2. To save a lot of time, create a [`.iex.exs`][iex-docs] file as shown in + [this gist][cams.iex.exs] to introduce a set of handy bindings and + aliases. + + We'll be using these bindings in the examples below. + + [example-repo]: https://github.com/aviabird/gringotts_example + [iex-docs]: https://hexdocs.pm/iex/IEx.html#module-the-iex-exs-file + [cams.iex.exs]: https://gist.github.com/oyeb/9a299df95cc13a87324e321faca5c9b8 ## Integrating with phoenix Refer the [GringottsPay][gpay-heroku-cams] website for an example of how to integrate CAMS with phoenix. The source is available [here][gpay-repo]. - + [gpay-repo]: https://github.com/aviabird/gringotts_payment [gpay-heroku-cams]: http://gringottspay.herokuapp.com/cams @@ -164,7 +159,7 @@ defmodule Gringotts.Gateways.Cams do month: 12, verification_code: "999", brand: "VISA"} - iex> money = %{value: Decimal.new(20), currency: "USD"} + iex> money = Money.new(20, :USD) iex> {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.Cams, money, card) ``` """ @@ -209,7 +204,7 @@ defmodule Gringotts.Gateways.Cams do month: 12, verification_code: "999", brand: "VISA"} - iex> money = %{value: Decimal.new(10), currency: "USD"} + iex> money = Money.new(10, :USD) iex> authorization = auth_result.authorization # authorization = "some_authorization_transaction_id" iex> {:ok, capture_result} = Gringotts.capture(Gringotts.Gateways.Cams, money, authorization) @@ -247,7 +242,7 @@ defmodule Gringotts.Gateways.Cams do month: 12, verification_code: "999", brand: "VISA"} - iex> money = %{value: Decimal.new(20), currency: "USD"} + iex> money = Money.new(20, :USD) iex> Gringotts.purchase(Gringotts.Gateways.Cams, money, card) ``` """ @@ -279,7 +274,7 @@ defmodule Gringotts.Gateways.Cams do ``` iex> capture_id = capture_result.authorization # capture_id = "some_capture_transaction_id" - iex> money = %{value: Decimal.new(20), currency: "USD"} + iex> money = Money.new(20, :USD) iex> Gringotts.refund(Gringotts.Gateways.Cams, money, capture_id) ``` """ diff --git a/lib/gringotts/gateways/monei.ex b/lib/gringotts/gateways/monei.ex index c297a1a3..eeef6ec6 100644 --- a/lib/gringotts/gateways/monei.ex +++ b/lib/gringotts/gateways/monei.ex @@ -126,56 +126,15 @@ defmodule Gringotts.Gateways.Monei do that you see in `Dashboard > Sub-accounts` as described [above](#module-registering-your-monei-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): - ``` - iex> alias Gringotts.{Response, CreditCard, Gateways.Monei} - iex> amount = %{value: Decimal.new(42), currency: "USD"} - iex> card = %CreditCard{first_name: "Harry", - last_name: "Potter", - number: "4200000000000000", - year: 2099, month: 12, - verification_code: "123", - brand: "VISA"} - iex> customer = %{"givenName": "Harry", - "surname": "Potter", - "merchantCustomerId": "the_boy_who_lived", - "sex": "M", - "birthDate": "1980-07-31", - "mobile": "+15252525252", - "email": "masterofdeath@ministryofmagic.gov", - "ip": "127.0.0.1", - "status": "NEW"} - iex> merchant = %{"name": "Ollivanders", - "city": "South Side", - "street": "Diagon Alley", - "state": "London", - "country": "GB", - "submerchantId": "Makers of Fine Wands since 382 B.C."} - iex> billing = %{"street1": "301, Gryffindor", - "street2": "Hogwarts School of Witchcraft and Wizardry, Hogwarts Castle", - "city": "Highlands", - "state": "Scotland", - "country": "GB"} - iex> shipping = %{"street1": "301, Gryffindor", - "street2": "Hogwarts School of Witchcraft and Wizardry, Hogwarts Castle", - "city": "Highlands", - "state": "Scotland", - "country": "GB", - "method": "SAME_DAY_SERVICE", - "comment": "For our valued customer, Mr. Potter"} - iex> opts = [customer: customer, - merchant: merchant, - billing: billing, - shipping: shipping, - category: "EC", - custom: %{"voldemort": "he who must not be named"}, - register: true] - ``` - - We'll be using these in the examples below. + 2. To save a lot of time, create a [`.iex.exs`][iex-docs] file as shown in + [this gist][monei.iex.exs] to introduce a set of handy bindings and + aliases. + + We'll be using these bindings in the examples below. [example-repo]: https://github.com/aviabird/gringotts_example + [iex-docs]: https://hexdocs.pm/iex/IEx.html#module-the-iex-exs-file + [monei.iex.exs]: https://gist.github.com/oyeb/a2e2ac5986cc90a12a6136f6bf1357e5 ## TODO @@ -251,7 +210,7 @@ defmodule Gringotts.Gateways.Monei do The following example shows how one would (pre) authorize a payment of $42 on a sample `card`. - iex> amount = %{value: Decimal.new(42), currency: "USD"} + iex> amount = Money.new(42, :USD) 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.Monei, amount, card, opts) iex> auth_result.id # This is the authorization ID @@ -289,7 +248,7 @@ defmodule Gringotts.Gateways.Monei do The following example shows how one would (partially) capture a previously authorized a payment worth $35 by referencing the obtained authorization `id`. - iex> amount = %{value: Decimal.new(35), currency: "USD"} + iex> amount = Money.new(35, :USD) iex> {:ok, capture_result} = Gringotts.capture(Gringotts.Gateways.Monei, amount, auth_result.id, opts) """ @spec capture(String.t(), Money.t(), keyword) :: {:ok | :error, Response.t()} @@ -323,7 +282,7 @@ defmodule Gringotts.Gateways.Monei do The following example shows how one would process a payment worth $42 in one-shot, without (pre) authorization. - iex> amount = %{value: Decimal.new(42), currency: "USD"} + iex> amount = Money.new(42, :USD) 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.Monei, amount, card, opts) iex> purchase_result.token # This is the registration ID/token @@ -357,7 +316,7 @@ defmodule Gringotts.Gateways.Monei do The following example shows how one would (completely) refund a previous purchase (and similarily for captures). - iex> amount = %{value: Decimal.new(42), currency: "USD"} + iex> amount = Money.new(42, :USD) iex> {:ok, refund_result} = Gringotts.refund(Gringotts.Gateways.Monei, purchase_result.id, amount) """ @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response.t()} diff --git a/lib/gringotts/gateways/trexle.ex b/lib/gringotts/gateways/trexle.ex index fba77838..523d4c2f 100644 --- a/lib/gringotts/gateways/trexle.ex +++ b/lib/gringotts/gateways/trexle.ex @@ -66,35 +66,16 @@ defmodule Gringotts.Gateways.Trexle do that as described [above](#module-registering-your-trexle-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): - ``` - iex> alias Gringotts.{Response, CreditCard, Gateways.Trexle} - iex> card = %CreditCard{ - first_name: "Harry", - last_name: "Potter", - number: "4200000000000000", - year: 2099, month: 12, - verification_code: "123", - brand: "VISA"} - iex> address = %Address{ - street1: "301, Gryffindor", - street2: "Hogwarts School of Witchcraft and Wizardry, Hogwarts Castle", - city: "Highlands", - region: "SL", - country: "GB", - postal_code: "11111", - phone: "(555)555-5555"} - iex> options = [email: "masterofdeath@ministryofmagic.gov", - ip_address: "127.0.0.1", - billing_address: address, - description: "For our valued customer, Mr. Potter"] - ``` + 2. To save a lot of time, create a [`.iex.exs`][iex-docs] file as shown in + [this gist][trexle.iex.exs] to introduce a set of handy bindings and + aliases. - We'll be using these in the examples below. + We'll be using these bindings in the examples below. [example-repo]: https://github.com/aviabird/gringotts_example - [gs]: # + [iex-docs]: https://hexdocs.pm/iex/IEx.html#module-the-iex-exs-file + [trexle.iex.exs]: https://gist.github.com/oyeb/055f40e9ad4102f5480febd2cfa00787 + [gs]: https://github.com/aviabird/gringotts/wiki """ @base_url "https://core.trexle.com/api/v1/" @@ -120,7 +101,7 @@ defmodule Gringotts.Gateways.Trexle do a sample `card`. ``` - iex> amount = %{value: Decimal.new(100),currency: "USD") + iex> amount = Money.new(10, :USD) iex> card = %CreditCard{ first_name: "Harry", last_name: "Potter", @@ -170,7 +151,7 @@ defmodule Gringotts.Gateways.Trexle do authorized a payment worth $10 by referencing the obtained `charge_token`. ``` - iex> amount = %{value: Decimal.new(100),currency: "USD") + iex> amount = Money.new(10, :USD) iex> token = "some-real-token" iex> Gringotts.capture(Gringotts.Gateways.Trexle, token, amount) ``` @@ -194,7 +175,7 @@ defmodule Gringotts.Gateways.Trexle do one-shot, without (pre) authorization. ``` - iex> amount = %{value: Decimal.new(100),currency: "USD") + iex> amount = Money.new(10, :USD) iex> card = %CreditCard{ first_name: "Harry", last_name: "Potter", @@ -242,7 +223,7 @@ defmodule Gringotts.Gateways.Trexle do `purchase/3` (and similarily for `capture/3`s). ``` - iex> amount = %{value: Decimal.new(100),currency: "USD") + iex> amount = Money.new(10, :USD) iex> token = "some-real-token" iex> Gringotts.refund(Gringotts.Gateways.Trexle, amount, token) ``` diff --git a/lib/mix/new.ex b/lib/mix/new.ex index c37fd7d0..02f4058b 100644 --- a/lib/mix/new.ex +++ b/lib/mix/new.ex @@ -6,10 +6,12 @@ defmodule Mix.Tasks.Gringotts.New do @moduledoc """ Generates a barebones implementation for a gateway. - It expects the (brand) name of the gateway as argument. This will not - necessarily be the module name, but we recommend the name be capitalized. + It expects the (brand) name of the gateway as argument and we recommend that + it be capitalized. *This will not necessarily be the module name*. - mix gringotts.new NAME [-m, --module MODULE] [--url URL] + ``` + mix gringotts.new NAME [-m, --module MODULE] [-f, --file FILENAME] [--url URL] + ``` A barebones implementation of the gateway will be created along with skeleton mock and integration tests in `lib/gringotts/gateways/`. The command will @@ -22,6 +24,7 @@ defmodule Mix.Tasks.Gringotts.New do > prompts. * `-m` `--module` - The module name for the Gateway. + * `-f` `--file` - The filename. * `--url` - The homepage of the gateway. ## Examples @@ -30,10 +33,10 @@ defmodule Mix.Tasks.Gringotts.New do The prompts for this will be: ``` - MODULE = `Foobar` - URL = `https://www.foobar.com` + MODULE = "Foobar" + URL = "https://www.foobar.com" + FILENAME = "foo_bar.ex" ``` - and the filename will be `foo_bar.ex` """ use Mix.Task @@ -45,20 +48,26 @@ Comma separated list of required configuration keys: > } def run(args) do - {key_list, [name], []} = + {key_list, name, []} = OptionParser.parse( args, - switches: [module: :string, url: :string], - aliases: [m: :module] + switches: [module: :string, url: :string, file: :string], + aliases: [m: :module, f: :file] ) Mix.Shell.IO.info("Generating barebones implementation for #{name}.") Mix.Shell.IO.info("Hit enter to select the suggestion.") + module_suggestion = + name |> String.split() |> Enum.map(&String.capitalize(&1)) |> Enum.join("") + module_name = case Keyword.fetch(key_list, :module) do - :error -> prompt_with_suggestion("\nModule name", String.capitalize(name)) - {:ok, mod_name} -> mod_name + :error -> + prompt_with_suggestion("\nModule name", module_suggestion) + + {:ok, mod_name} -> + mod_name end url = @@ -66,18 +75,28 @@ Comma separated list of required configuration keys: :error -> prompt_with_suggestion( "\nHomepage URL", - "https://www.#{String.Casing.downcase(name)}.com" + "https://www.#{String.downcase(module_suggestion)}.com" ) {:ok, url} -> url end - file_name = prompt_with_suggestion("\nFilename", Macro.underscore(name)) + file_name = + case Keyword.fetch(key_list, :file) do + :error -> + prompt_with_suggestion("\nFilename", Macro.underscore(module_name) <> ".ex") + + {:ok, filename} -> + filename + end + + file_base_name = String.slice(file_name, 0..-4) required_keys = case Mix.Shell.IO.prompt(@long_msg) |> String.trim() do - "" -> [] + "" -> + [] keys -> String.split(keys, ",") |> Enum.map(&String.trim(&1)) |> Enum.map(&String.to_atom(&1)) @@ -87,10 +106,12 @@ Comma separated list of required configuration keys: gateway: name, gateway_module: module_name, gateway_underscore: file_name, + # The key :gateway_filename is not used in any template as of now. + gateway_filename: "#{file_name}", required_config_keys: required_keys, gateway_url: url, - mock_test_filename: file_name <> "_test", - mock_response_filename: file_name <> "_mock" + mock_test_filename: "#{file_base_name}_test.exs", + mock_response_filename: "#{file_base_name}_mock.exs" ] if Mix.Shell.IO.yes?( @@ -101,12 +122,12 @@ Comma separated list of required configuration keys: mock_response = EEx.eval_file("templates/mock_response.eex", bindings) integration = EEx.eval_file("templates/integration.eex", bindings) - create_file("lib/gringotts/gateways/#{bindings[:gateway_underscore]}.ex", gateway) - create_file("test/integration/gateways/#{bindings[:mock_test_filename]}.exs", integration) + create_file("lib/gringotts/gateways/#{bindings[:gateway_filename]}", gateway) + create_file("test/integration/gateways/#{bindings[:mock_test_filename]}", integration) if Mix.Shell.IO.yes?("\nAlso create empty mock test suite?\n>") do - create_file("test/gateways/#{bindings[:mock_test_filename]}.exs", mock) - create_file("test/mocks/#{bindings[:mock_response_filename]}.exs", mock_response) + create_file("test/gateways/#{bindings[:mock_test_filename]}", mock) + create_file("test/mocks/#{bindings[:mock_response_filename]}", mock_response) end else Mix.Shell.IO.info("Doing nothing, bye!") diff --git a/mix.exs b/mix.exs index f63edfc6..ef54cc7f 100644 --- a/mix.exs +++ b/mix.exs @@ -58,16 +58,16 @@ defmodule Gringotts.Mixfile do {:ex_money, "~> 1.1.0", only: [:dev, :test], optional: true}, # docs and tests - {:ex_doc, "~> 0.16", only: :dev, runtime: false}, + {:ex_doc, "~> 0.18", only: :dev, runtime: false}, {:mock, "~> 0.3.0", only: :test}, {:bypass, "~> 0.8", only: :test}, - {:excoveralls, "~> 0.7", only: :test}, + {:excoveralls, "~> 0.8", only: :test}, # various analyses tools {:credo, "~> 0.3", only: [:dev, :test]}, {:inch_ex, "~> 0.5", only: :docs}, {:dialyxir, "~> 0.3", only: :dev}, - {:timex, "~> 3.1"} + {:timex, "~> 3.2"} ] end diff --git a/mix.lock b/mix.lock index da4109e3..4e785b03 100644 --- a/mix.lock +++ b/mix.lock @@ -1,4 +1,5 @@ -%{"abnf2": {:hex, :abnf2, "0.1.2", "6f8792b8ac3288dba5fc889c2bceae9fe78f74e1a7b36bea9726ffaa9d7bef95", [:mix], [], "hexpm"}, +%{ + "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.0.0", "a0c0e475107135f76b8c1d5bc7efb33cd3815cb3cf3dea7aefdd174dabead064", [:rebar3], [], "hexpm"}, @@ -17,8 +18,8 @@ "ex_money": {:hex, :ex_money, "1.1.2", "4336192f1ac263900dfb4f63c1f71bc36a7cdee5d900e81937d3213be3360f9f", [: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"}, "excoveralls": {:hex, :excoveralls, "0.8.0", "99d2691d3edf8612f128be3f9869c4d44b91c67cec92186ce49470ae7a7404cf", [: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"}, - "gettext": {:hex, :gettext, "0.14.0", "1a019a2e51d5ad3d126efe166dcdf6563768e5d06c32a99ad2281a1fa94b4c72", [:mix], [], "hexpm"}, - "hackney": {:hex, :hackney, "1.10.1", "c38d0ca52ea80254936a32c45bb7eb414e7a96a521b4ce76d00a69753b157f21", [: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"}, + "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"}, "idna": {:hex, :idna, "5.1.0", "d72b4effeb324ad5da3cab1767cb16b17939004e789d8c0ad5b70f3cea20c89a", [: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"}, @@ -32,7 +33,8 @@ "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.1.25", "6002dae5432f749d1c93e2cd103eb73cecb53e50d2c885349e8e4146fc96bd44", [: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"}, + "timex": {:hex, :timex, "3.2.1", "639975eac45c4c08c2dbf7fc53033c313ff1f94fad9282af03619a3826493612", [: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"}} + "xml_builder": {:hex, :xml_builder, "2.1.0", "c249d5339427c13cae11e9d9d0e8b40d25d228b9ecc54029f24017385e60280b", [:mix], [], "hexpm"}, +} diff --git a/templates/mock_response.eex b/templates/mock_response.eex index d4ad1f5b..b50bc667 100644 --- a/templates/mock_response.eex +++ b/templates/mock_response.eex @@ -1,6 +1,6 @@ defmodule Gringotts.Gateways.<%= gateway_module <> "Mock"%> do - # The module should include mock responses for test cases in <%= mock_test_filename <> ".exs"%>. + # The module should include mock responses for test cases in <%= mock_test_filename %>. # e.g. # def successful_purchase do # {:ok, %HTTPoison.Response{body: ~s[{data: "successful_purchase"}]} diff --git a/templates/test.eex b/templates/test.eex index 2fc65a79..93c0b62d 100644 --- a/templates/test.eex +++ b/templates/test.eex @@ -2,13 +2,13 @@ defmodule Gringotts.Gateways.<%= gateway_module <> "Test" %> do # The file contains mocked tests for <%= gateway_module%> # We recommend using [mock][1] for this, you can place the mock responses from - # the Gateway in `test/mocks/<%= mock_response_filename%>.exs` file, which has also been + # the Gateway in `test/mocks/<%= mock_response_filename %>` file, which has also been # generated for you. # # [1]: https://github.com/jjh42/mock # Load the mock response file before running the tests. - Code.require_file "../mocks/<%= mock_response_filename <> ".exs"%>", __DIR__ + Code.require_file "../mocks/<%= mock_response_filename %>", __DIR__ use ExUnit.Case, async: false alias Gringotts.Gateways.<%= gateway_module%> diff --git a/test/gateways/bogus_test.exs b/test/gateways/bogus_test.exs index 5810dfc0..041c1469 100644 --- a/test/gateways/bogus_test.exs +++ b/test/gateways/bogus_test.exs @@ -4,9 +4,12 @@ defmodule Gringotts.Gateways.BogusTest do alias Gringotts.Response alias Gringotts.Gateways.Bogus, as: Gateway + @some_id "some_arbitrary_id" + @amount Money.new(5, :USD) + test "authorize" do {:ok, %Response{id: id, success: success}} = - Gateway.authorize(10.95, :card, []) + Gateway.authorize(@amount, :card, []) assert success assert id != nil @@ -14,7 +17,7 @@ defmodule Gringotts.Gateways.BogusTest do test "purchase" do {:ok, %Response{id: id, success: success}} = - Gateway.purchase(10.95, :card, []) + Gateway.purchase(@amount, :card, []) assert success assert id != nil @@ -22,7 +25,7 @@ defmodule Gringotts.Gateways.BogusTest do test "capture" do {:ok, %Response{id: id, success: success}} = - Gateway.capture(1234, 5, []) + Gateway.capture(@some_id, @amount, []) assert success assert id != nil @@ -30,7 +33,7 @@ defmodule Gringotts.Gateways.BogusTest do test "void" do {:ok, %Response{id: id, success: success}} = - Gateway.void(1234, []) + Gateway.void(@some_id, []) assert success assert id != nil @@ -45,7 +48,7 @@ defmodule Gringotts.Gateways.BogusTest do test "unstore with customer" do {:ok, %Response{success: success}} = - Gateway.unstore(1234, []) + Gateway.unstore(@some_id, []) assert success end From c1847ac542719527b4320936dae6f77a138eb62d Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Thu, 15 Mar 2018 20:59:37 +0530 Subject: [PATCH 09/24] [monei] Test fixes (#116) * Ignore some optional params for RF, RV, CP Some optional params like billing, customer, merchant must not be expanded in case of capture, refund and void. * Improved mock tests, fixes #98 Mock tests now mostly check if the request is correctly built. Since most requests have common parameters, they are not checked everywhere. * Improve integration tests (more cases), fixes #108 * Integration tests no longer use the worker as a workaround for #8 * Added more test cases, can possibly be improved using describe blocks with local setup. * There are almost no assertions and it is expected that errors will bubble up to the pattern matches. --- lib/gringotts/gateways/monei.ex | 67 +++++++++----- mix.lock | 12 +-- test/gateways/monei_test.exs | 110 +++++++++++++++-------- test/integration/gateways/monei_test.exs | 104 +++++++++++++-------- 4 files changed, 195 insertions(+), 98 deletions(-) diff --git a/lib/gringotts/gateways/monei.ex b/lib/gringotts/gateways/monei.ex index eeef6ec6..2d08080c 100644 --- a/lib/gringotts/gateways/monei.ex +++ b/lib/gringotts/gateways/monei.ex @@ -338,6 +338,8 @@ defmodule Gringotts.Gateways.Monei do which can be used to effectively process _One-Click_ and _Recurring_ payments, and return a registration token for reference. + The registration token is available in the `Response.id` field. + It is recommended to associate these details with a "Customer" by passing customer details in the `opts`. @@ -352,7 +354,8 @@ defmodule Gringotts.Gateways.Monei do future use. iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} - iex> {:ok, store_result} = Gringotts.store(Gringotts.Gateways.Monei, card, []) + iex> {:ok, store_result} = Gringotts.store(Gringotts.Gateways.Monei, card) + iex> store_result.id # This is the registration token """ @spec store(CreditCard.t(), keyword) :: {:ok | :error, Response.t()} def store(%CreditCard{} = card, opts) do @@ -438,7 +441,7 @@ defmodule Gringotts.Gateways.Monei do defp commit(:post, endpoint, params, opts) do url = "#{base_url(opts)}/#{version(opts)}/#{endpoint}" - case expand_params(opts, params[:paymentType]) do + case expand_params(Keyword.delete(opts, :config), params[:paymentType]) do {:error, reason} -> {:error, Response.error(reason: reason)} @@ -528,16 +531,16 @@ defmodule Gringotts.Gateways.Monei do else: {:halt, {:error, "Invalid currency"}} :customer -> - {:cont, acc ++ make("customer", v)} + {:cont, acc ++ make(action_type, "customer", v)} :merchant -> - {:cont, acc ++ make("merchant", v)} + {:cont, acc ++ make(action_type, "merchant", v)} :billing -> - {:cont, acc ++ make("billing", v)} + {:cont, acc ++ make(action_type, "billing", v)} :shipping -> - {:cont, acc ++ make("shipping", v)} + {:cont, acc ++ make(action_type, "shipping", v)} :invoice_id -> {:cont, [{"merchantInvoiceId", v} | acc]} @@ -549,23 +552,16 @@ defmodule Gringotts.Gateways.Monei do {:cont, [{"transactionCategory", v} | acc]} :shipping_customer -> - {:cont, acc ++ make("shipping.customer", v)} + {:cont, acc ++ make(action_type, "shipping.customer", v)} :custom -> {:cont, acc ++ make_custom(v)} :register -> - { - :cont, - if action_type in ["PA", "DB"] do - [{"createRegistration", true} | acc] - else - acc - end - } - - _ -> - {:cont, acc} + {:cont, acc ++ make(action_type, :register, v)} + + unsupported -> + {:halt, {:error, "Unsupported optional param '#{unsupported}'"}} end end) end @@ -574,8 +570,39 @@ defmodule Gringotts.Gateways.Monei do currency in @supported_currencies end - defp make(prefix, param) do - Enum.into(param, [], fn {k, v} -> {"#{prefix}.#{k}", v} end) + defp parse_response(%{"result" => result} = data) do + {address, zip_code} = @avs_code_translator[result["avsResponse"]] + + results = [ + code: result["code"], + description: result["description"], + risk: data["risk"]["score"], + cvc_result: @cvc_code_translator[result["cvvResponse"]], + avs_result: [address: address, zip_code: zip_code], + raw: data, + token: data["registrationId"] + ] + + filtered = Enum.filter(results, fn {_, v} -> v != nil end) + verify(filtered) + end + + defp verify(results) do + if String.match?(results[:code], ~r{^(000\.000\.|000\.100\.1|000\.[36])}) do + {:ok, results} + else + {:error, [{:reason, results[:description]} | results]} + end + end + + defp make(action_type, _prefix, _param) when action_type in ["CP", "RF", "RV"], do: [] + defp make(action_type, prefix, param) do + case prefix do + :register -> + if action_type in ["PA", "DB"], do: [createRegistration: true], else: [] + + _ -> Enum.into(param, [], fn {k, v} -> {"#{prefix}.#{k}", v} end) + end end defp make_custom(custom_map) do diff --git a/mix.lock b/mix.lock index 4e785b03..aaaa5cc1 100644 --- a/mix.lock +++ b/mix.lock @@ -12,11 +12,11 @@ "earmark": {:hex, :earmark, "1.2.4", "99b637c62a4d65a20a9fb674b8cffb8baa771c04605a80c911c4418c69b75439", [:mix], [], "hexpm"}, "elixir_xml_to_map": {:hex, :elixir_xml_to_map, "0.1.1", "57e924cd11731947bfd245ce57d0b8dd8b7168bf8edb20cd156a2982ca96fdfa", [:mix], [{:erlsom, "~>1.4", [hex: :erlsom, repo: "hexpm", optional: false]}], "hexpm"}, "erlsom": {:hex, :erlsom, "1.4.1", "53dbacf35adfea6f0714fd0e4a7b0720d495e88c5e24e12c5dc88c7b62bd3e49", [:rebar3], [], "hexpm"}, - "ex_cldr": {:hex, :ex_cldr, "1.1.0", "26f4a206307770b70139214ab820c5ed1f6241eb3394dd0db216ff95bf7e213a", [: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]}, {:poison, "~> 2.1 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, - "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "1.1.0", "75904f202ca602eca5f3af572d56ed3d4a51543fecd08c9ab626ae2d876f44da", [:mix], [{:decimal, "~> 1.4", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 1.0", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:poison, "~> 2.1 or ~> 3.1", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, - "ex_doc": {:hex, :ex_doc, "0.18.1", "37c69d2ef62f24928c1f4fdc7c724ea04aecfdf500c4329185f8e3649c915baf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, - "ex_money": {:hex, :ex_money, "1.1.2", "4336192f1ac263900dfb4f63c1f71bc36a7cdee5d900e81937d3213be3360f9f", [: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"}, - "excoveralls": {:hex, :excoveralls, "0.8.0", "99d2691d3edf8612f128be3f9869c4d44b91c67cec92186ce49470ae7a7404cf", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, + "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_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"}, + "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"}, "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"}, @@ -29,7 +29,7 @@ "mime": {:hex, :mime, "1.2.0", "78adaa84832b3680de06f88f0997e3ead3b451a440d183d688085be2d709b534", [: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"}, - "plug": {:hex, :plug, "1.4.3", "236d77ce7bf3e3a2668dc0d32a9b6f1f9b1f05361019946aae49874904be4aed", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"}, + "plug": {:hex, :plug, "1.5.0", "224b25b4039bedc1eac149fb52ed456770b9678bbf0349cdd810460e1e09195b", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.1", [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"}, diff --git a/test/gateways/monei_test.exs b/test/gateways/monei_test.exs index 8d3a11ec..3aaa88a1 100644 --- a/test/gateways/monei_test.exs +++ b/test/gateways/monei_test.exs @@ -39,7 +39,7 @@ defmodule Gringotts.Gateways.MoneiTest do birthDate: "1980-07-31", mobile: "+15252525252", email: "masterofdeath@ministryofmagic.gov", - ip: "1.1.1", + ip: "127.0.0.1", status: "NEW" } @merchant %{ @@ -96,7 +96,7 @@ defmodule Gringotts.Gateways.MoneiTest do "card":{ "bin":"420000", "last4Digits":"0000", - "holder":"Jo Doe", + "holder":"Harry Potter", "expiryMonth":"12", "expiryYear":"2099" } @@ -123,16 +123,24 @@ defmodule Gringotts.Gateways.MoneiTest do end test "when MONEI is down or unreachable.", %{bypass: bypass, auth: auth} do - Bypass.expect_once(bypass, fn conn -> - Plug.Conn.resp(conn, 200, @auth_success) - end) - Bypass.down(bypass) {:error, response} = Gateway.authorize(@amount42, @card, config: auth) assert response.reason == "network related failure" - Bypass.up(bypass) - {:ok, _} = Gateway.authorize(@amount42, @card, config: auth) + end + + test "that all auth info is picked.", %{bypass: bypass, auth: auth} do + Bypass.expect_once(bypass, "POST", "/v1/payments", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert params["authentication.entityId"] == "some_secret_entity_id" + assert params["authentication.password"] == "some_secret_password" + assert params["authentication.userId"] == "some_secret_user_id" + Plug.Conn.resp(conn, 200, @auth_success) + end) + + {:ok, response} = Gateway.purchase(@amount42, @card, config: auth) + assert response.gateway_code == "000.100.110" end test "with all extra_params.", %{bypass: bypass, auth: auth} do @@ -142,20 +150,21 @@ defmodule Gringotts.Gateways.MoneiTest do ] Bypass.expect_once(bypass, "POST", "/v1/payments", fn conn -> - conn_ = parse(conn) - assert conn_.body_params["createRegistration"] == "true" - assert conn_.body_params["customParameters"] == @extra_opts[:custom] - assert conn_.body_params["merchantInvoiceId"] == randoms[:invoice_id] - assert conn_.body_params["merchantTransactionId"] == randoms[:transaction_id] - assert conn_.body_params["transactionCategory"] == @extra_opts[:category] - assert conn_.body_params["customer.merchantCustomerId"] == @customer[:merchantCustomerId] - - assert conn_.body_params["shipping.customer.merchantCustomerId"] == + p_conn = parse(conn) + params = p_conn.body_params + assert params["createRegistration"] == "true" + assert params["customParameters"] == @extra_opts[:custom] + assert params["merchantInvoiceId"] == randoms[:invoice_id] + assert params["merchantTransactionId"] == randoms[:transaction_id] + assert params["transactionCategory"] == @extra_opts[:category] + assert params["customer.merchantCustomerId"] == @customer[:merchantCustomerId] + + assert params["shipping.customer.merchantCustomerId"] == @customer[:merchantCustomerId] - assert conn_.body_params["merchant.submerchantId"] == @merchant[:submerchantId] - assert conn_.body_params["billing.city"] == @billing[:city] - assert conn_.body_params["shipping.method"] == @shipping[:method] + assert params["merchant.submerchantId"] == @merchant[:submerchantId] + assert params["billing.city"] == @billing[:city] + assert params["shipping.method"] == @shipping[:method] Plug.Conn.resp(conn, 200, @register_success) end) @@ -165,7 +174,7 @@ defmodule Gringotts.Gateways.MoneiTest do assert response.token == "8a82944a60e09c550160e92da144491e" end - test "when card has expired.", %{bypass: bypass, auth: auth} do + test "when we get non-json.", %{bypass: bypass, auth: auth} do Bypass.expect_once(bypass, "POST", "/v1/payments", fn conn -> Plug.Conn.resp(conn, 400, "") end) @@ -177,6 +186,11 @@ defmodule Gringotts.Gateways.MoneiTest do describe "authorize" do test "when all is good.", %{bypass: bypass, auth: auth} do Bypass.expect(bypass, "POST", "/v1/payments", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "42.00" + assert params["currency"] == "USD" + assert params["paymentType"] == "PA" Plug.Conn.resp(conn, 200, @auth_success) end) @@ -188,6 +202,11 @@ defmodule Gringotts.Gateways.MoneiTest do describe "purchase" do test "when all is good.", %{bypass: bypass, auth: auth} do Bypass.expect_once(bypass, "POST", "/v1/payments", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "42.00" + assert params["currency"] == "USD" + assert params["paymentType"] == "DB" Plug.Conn.resp(conn, 200, @auth_success) end) @@ -197,8 +216,9 @@ defmodule Gringotts.Gateways.MoneiTest do test "with createRegistration.", %{bypass: bypass, auth: auth} do Bypass.expect_once(bypass, "POST", "/v1/payments", fn conn -> - conn_ = parse(conn) - assert conn_.body_params["createRegistration"] == "true" + p_conn = parse(conn) + params = p_conn.body_params + assert params["createRegistration"] == "true" Plug.Conn.resp(conn, 200, @register_success) end) @@ -211,6 +231,14 @@ defmodule Gringotts.Gateways.MoneiTest do describe "store" do test "when all is good.", %{bypass: bypass, auth: auth} do Bypass.expect_once(bypass, "POST", "/v1/registrations", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + params["card.cvv"] == "123" + params["card.expiryMonth"] == "12" + params["card.expiryYear"] == "2099" + params["card.holder"] == "Harry Potter" + params["card.number"] == "4200000000000000" + params["paymentBrand"] == "VISA" Plug.Conn.resp(conn, 200, @store_success) end) @@ -226,6 +254,11 @@ defmodule Gringotts.Gateways.MoneiTest do "POST", "/v1/payments/7214344242e11af79c0b9e7b4f3f6234", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "42.00" + assert params["currency"] == "USD" + assert params["paymentType"] == "CP" Plug.Conn.resp(conn, 200, @auth_success) end ) @@ -242,8 +275,9 @@ defmodule Gringotts.Gateways.MoneiTest do "POST", "/v1/payments/7214344242e11af79c0b9e7b4f3f6234", fn conn -> - conn_ = parse(conn) - assert :error == Map.fetch(conn_.body_params, "createRegistration") + p_conn = parse(conn) + params = p_conn.body_params + assert :error == Map.fetch(params, "createRegistration") Plug.Conn.resp(conn, 200, @auth_success) end ) @@ -267,6 +301,11 @@ defmodule Gringotts.Gateways.MoneiTest do "POST", "/v1/payments/7214344242e11af79c0b9e7b4f3f6234", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "3.00" + assert params["currency"] == "USD" + assert params["paymentType"] == "RF" Plug.Conn.resp(conn, 200, @auth_success) end ) @@ -284,6 +323,11 @@ defmodule Gringotts.Gateways.MoneiTest do "DELETE", "/v1/registrations/7214344242e11af79c0b9e7b4f3f6234", fn conn -> + p_conn = parse(conn) + params = p_conn.query_params + assert params["authentication.entityId"] == "some_secret_entity_id" + assert params["authentication.password"] == "some_secret_password" + assert params["authentication.userId"] == "some_secret_user_id" Plug.Conn.resp(conn, 200, "") end ) @@ -300,6 +344,11 @@ defmodule Gringotts.Gateways.MoneiTest do "POST", "/v1/payments/7214344242e11af79c0b9e7b4f3f6234", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert :error == Map.fetch(params, :amount) + assert :error == Map.fetch(params, :currency) + assert params["paymentType"] == "RV" Plug.Conn.resp(conn, 200, @auth_success) end ) @@ -309,17 +358,6 @@ defmodule Gringotts.Gateways.MoneiTest do end end - @tag :skip - test "respond various scenarios, can't test a private function." do - json_200 = %HTTPoison.Response{body: @auth_success, status_code: 200} - json_not_200 = %HTTPoison.Response{body: @auth_success, status_code: 300} - html_200 = %HTTPoison.Response{body: ~s[\n], status_code: 200} - html_not_200 = %HTTPoison.Response{body: ~s[ - assert response.gateway_code == "000.100.110" - - assert response.message == - "Request successfully processed in 'Merchant in Integrator Test Mode'" - - assert String.length(response.id) == 32 - + test "[authorize] without tokenisation", %{opts: opts} do + with {:ok, auth_result} <- Gateway.authorize(@amount, @card, opts), + {:ok, _capture_result} <- Gateway.capture(auth_result.id, @amount, opts) do + "yay!" + else {:error, _err} -> flunk() end end - @tag :skip - test "capture", %{opts: _opts} do - case Gringotts.capture(Gateway, "s", @amount) do - {:ok, response} -> - assert response.gateway_code == "000.100.110" - - assert response.message == - "Request successfully processed in 'Merchant in Integrator Test Mode'" - - assert String.length(response.id) == 32 + test "[authorize -> capture] with tokenisation", %{opts: opts} do + with {:ok, auth_result} <- Gateway.authorize(@amount, @card, opts ++ [register: true]), + {:ok, _registration_token} <- Map.fetch(auth_result, :token), + {:ok, _capture_result} <- Gateway.capture(auth_result.id, @amount, opts) do + "yay!" + else {:error, _err} -> + flunk() + end + end - {:error, _err} -> + test "[authorize -> void]", %{opts: opts} do + with {:ok, auth_result} <- Gateway.authorize(@amount, @card, opts), + {:ok, _void_result} <- Gateway.void(auth_result.id, opts) do + "yay!" + else {:error, _err} -> flunk() end end - test "purchase", %{opts: opts} do - case Gringotts.purchase(Gateway, @amount, @card, opts) do - {:ok, response} -> - assert response.gateway_code == "000.100.110" + test "[purchase/capture -> void]", %{opts: opts} do + with {:ok, purchase_result} <- Gateway.purchase(@amount, @card, opts), + {:ok, _void_result} <- Gateway.void(purchase_result.id, opts) do + "yay!" + else {:error, _err} -> + flunk() + end + end - assert response.message == - "Request successfully processed in 'Merchant in Integrator Test Mode'" + test "[purchase/capture -> refund] (partial)", %{opts: opts} do + with {:ok, purchase_result} <- Gateway.purchase(@amount, @card, opts), + {:ok, _refund_result} <- Gateway.refund(@sub_amount, purchase_result.id, opts) do + "yay!" + else {:error, _err} -> + flunk() + end + end - assert String.length(response.id) == 32 + test "[store]", %{opts: opts} do + assert {:ok, _store_result} = Gateway.store(@card, opts) + end - {:error, _err} -> + @tag :skip + test "[store -> unstore]", %{opts: opts} do + with {:ok, store_result} <- Gateway.store(@card, opts), + {:ok, _unstore_result} <- Gateway.unstore(store_result.id, opts) do + "yay!" + else {:error, _err} -> flunk() end end + + test "[purchase]", %{opts: opts} do + assert {:ok, _response} = Gateway.purchase(@amount, @card, opts) + end + + test "Environment setup" do + config = Application.get_env(:gringotts, Gringotts.Gateways.Monei) + assert config[:adapter] == Gringotts.Gateways.Monei + end end From 9df91f3b8e7cea8f74d53794a2272380ab4ce623 Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Mon, 19 Mar 2018 11:45:40 +0530 Subject: [PATCH 10/24] Fix gringotts.new Option.parse call. (#125) --- lib/mix/new.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/new.ex b/lib/mix/new.ex index 02f4058b..7b4fcd2e 100644 --- a/lib/mix/new.ex +++ b/lib/mix/new.ex @@ -48,7 +48,7 @@ Comma separated list of required configuration keys: > } def run(args) do - {key_list, name, []} = + {key_list, [name], []} = OptionParser.parse( args, switches: [module: :string, url: :string, file: :string], From 10f5058874e3d12eec8bffe839d66721f19647ce Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Mon, 29 Jan 2018 22:09:12 +0530 Subject: [PATCH 11/24] Adapts Stripe with the money protocol * Fixes #5 and #109 * Moved the stripe_test to integration. * Fixed credo issue in money integration test Changes to MONEI * Removed unnecessary clauses from MONEI * Re-formatted source. --- lib/gringotts/gateways/base.ex | 35 +++---- lib/gringotts/gateways/monei.ex | 53 +++-------- lib/gringotts/gateways/stripe.ex | 107 +++++++++++----------- test/gateways/monei_test.exs | 12 +-- test/gateways/stripe_test.exs | 58 ------------ test/integration/gateways/stripe_test.exs | 44 +++++++++ test/integration/money.exs | 2 +- 7 files changed, 132 insertions(+), 179 deletions(-) delete mode 100644 test/gateways/stripe_test.exs create mode 100644 test/integration/gateways/stripe_test.exs diff --git a/lib/gringotts/gateways/base.ex b/lib/gringotts/gateways/base.ex index 92f8cfd2..be881992 100644 --- a/lib/gringotts/gateways/base.ex +++ b/lib/gringotts/gateways/base.ex @@ -1,4 +1,18 @@ defmodule Gringotts.Gateways.Base do + @moduledoc """ + Dummy implementation of the Gringotts API + + All gateway implementations must `use` this module as it provides (pseudo) + implementations for the all methods of the Gringotts API. + + In case `GatewayXYZ` does not implement `unstore`, the following call would + not raise an error: + ``` + Gringotts.unstore(GatewayXYZ, "some_registration_id") + ``` + because this module provides an implementation. + """ + alias Gringotts.Response defmacro __using__(_) do @@ -38,27 +52,6 @@ defmodule Gringotts.Gateways.Base do not_implemented() end - defp http(method, path, params \\ [], opts \\ []) do - credentials = Keyword.get(opts, :credentials) - headers = [{"Content-Type", "application/x-www-form-urlencoded"}] - data = params_to_string(params) - - HTTPoison.request(method, path, data, headers, [hackney: [basic_auth: credentials]]) - end - - defp money_to_cents(amount) when is_float(amount) do - trunc(amount * 100) - end - - defp money_to_cents(amount) do - amount * 100 - end - - defp params_to_string(params) do - params |> Enum.filter(fn {_k, v} -> v != nil end) - |> URI.encode_query - end - @doc false defp not_implemented do {:error, Response.error(code: :not_implemented)} diff --git a/lib/gringotts/gateways/monei.ex b/lib/gringotts/gateways/monei.ex index 2d08080c..4d659656 100644 --- a/lib/gringotts/gateways/monei.ex +++ b/lib/gringotts/gateways/monei.ex @@ -153,15 +153,11 @@ defmodule Gringotts.Gateways.Monei do @base_url "https://test.monei-api.net" @default_headers ["Content-Type": "application/x-www-form-urlencoded", charset: "UTF-8"] - @supported_currencies [ - "AED", "AFN", "ANG", "AOA", "AWG", "AZN", "BAM", "BGN", "BRL", "BYN", "CDF", - "CHF", "CUC", "EGP", "EUR", "GBP", "GEL", "GHS", "MDL", "MGA", "MKD", "MWK", - "MZN", "NAD", "NGN", "NIO", "NOK", "NPR", "NZD", "PAB", "PEN", "PGK", "PHP", - "PKR", "PLN", "PYG", "QAR", "RSD", "RUB", "RWF", "SAR", "SCR", "SDG", "SEK", - "SGD", "SHP", "SLL", "SOS", "SRD", "STD", "SYP", "SZL", "THB", "TJS", "TOP", - "TRY", "TTD", "TWD", "TZS", "UAH", "UGX", "USD", "UYU", "UZS", "VND", "VUV", - "WST", "XAF", "XCD", "XOF", "XPF", "YER", "ZAR", "ZMW", "ZWL" - ] + @supported_currencies ~w(AED AFN ANG AOA AWG AZN BAM BGN BRL BYN CDF CHF CUC + EGP EUR GBP GEL GHS MDL MGA MKD MWK MZN NAD NGN NIO NOK NPR NZD PAB PEN PGK + PHP PKR PLN PYG QAR RSD RUB RWF SAR SCR SDG SEK SGD SHP SLL SOS SRD STD SYP + SZL THB TJS TOP TRY TTD TWD TZS UAH UGX USD UYU UZS VND VUV WST XAF XCD XOF + XPF YER ZAR ZMW ZWL) @version "v1" @@ -435,7 +431,6 @@ defmodule Gringotts.Gateways.Monei do ] end - # Makes the request to MONEI's network. @spec commit(atom, String.t(), keyword, keyword) :: {:ok | :error, Response.t()} defp commit(:post, endpoint, params, opts) do @@ -447,7 +442,10 @@ defmodule Gringotts.Gateways.Monei do validated_params -> url - |> HTTPoison.post({:form, params ++ validated_params ++ auth_params(opts)}, @default_headers) + |> HTTPoison.post( + {:form, params ++ validated_params ++ auth_params(opts)}, + @default_headers + ) |> respond end end @@ -458,7 +456,7 @@ defmodule Gringotts.Gateways.Monei do auth_params = auth_params(opts) query_string = auth_params |> URI.encode_query() - base_url <> "?" <> query_string + (base_url <> "?" <> query_string) |> HTTPoison.delete() |> respond end @@ -472,7 +470,7 @@ defmodule Gringotts.Gateways.Monei do common = [raw: body, status_code: 200] with {:ok, decoded_json} <- decode(body), - {:ok, results} <- parse_response(decoded_json) do + {:ok, results} <- parse_response(decoded_json) do {:ok, Response.success(common ++ results)} else {:not_ok, errors} -> @@ -570,38 +568,15 @@ defmodule Gringotts.Gateways.Monei do currency in @supported_currencies end - defp parse_response(%{"result" => result} = data) do - {address, zip_code} = @avs_code_translator[result["avsResponse"]] - - results = [ - code: result["code"], - description: result["description"], - risk: data["risk"]["score"], - cvc_result: @cvc_code_translator[result["cvvResponse"]], - avs_result: [address: address, zip_code: zip_code], - raw: data, - token: data["registrationId"] - ] - - filtered = Enum.filter(results, fn {_, v} -> v != nil end) - verify(filtered) - end - - defp verify(results) do - if String.match?(results[:code], ~r{^(000\.000\.|000\.100\.1|000\.[36])}) do - {:ok, results} - else - {:error, [{:reason, results[:description]} | results]} - end - end - defp make(action_type, _prefix, _param) when action_type in ["CP", "RF", "RV"], do: [] + defp make(action_type, prefix, param) do case prefix do :register -> if action_type in ["PA", "DB"], do: [createRegistration: true], else: [] - _ -> Enum.into(param, [], fn {k, v} -> {"#{prefix}.#{k}", v} end) + _ -> + Enum.into(param, [], fn {k, v} -> {"#{prefix}.#{k}", v} end) end end diff --git a/lib/gringotts/gateways/stripe.ex b/lib/gringotts/gateways/stripe.ex index 7c1befd2..94a8ce48 100644 --- a/lib/gringotts/gateways/stripe.ex +++ b/lib/gringotts/gateways/stripe.ex @@ -1,9 +1,8 @@ defmodule Gringotts.Gateways.Stripe do - @moduledoc """ Stripe gateway implementation. For reference see [Stripe's API documentation](https://stripe.com/docs/api). The following features of Stripe are implemented: - + | Action | Method | | ------ | ------ | | Pre-authorize | `authorize/3` | @@ -18,7 +17,7 @@ defmodule Gringotts.Gateways.Stripe do Most `Gringotts` API calls accept an optional `Keyword` list `opts` to supply optional arguments for transactions with the Stripe gateway. The following keys are supported: - + | Key | Remark | Status | | ---- | --- | ---- | | `currency` | | **Implemented** | @@ -38,18 +37,18 @@ defmodule Gringotts.Gateways.Stripe do | `default_source` | | Not implemented | | `email` | | Not implemented | | `shipping` | | Not implemented | - + ## Registering your Stripe account at `Gringotts` After [making an account on Stripe](https://stripe.com/), head to the dashboard and find your account `secrets` in the `API` section. - + ## Here's how the secrets map to the required configuration parameters for Stripe: | Config parameter | Stripe secret | | ------- | ---- | | `:secret_key` | **Secret key** | - + Your Application config must look something like this: - + config :gringotts, Gringotts.Gateways.Stripe, secret_key: "your_secret_key", default_currency: "usd" @@ -58,11 +57,12 @@ defmodule Gringotts.Gateways.Stripe do @base_url "https://api.stripe.com/v1" use Gringotts.Gateways.Base - use Gringotts.Adapter, required_config: [:secret_key, :default_currency] + use Gringotts.Adapter, required_config: [:secret_key] alias Gringotts.{ CreditCard, - Address + Address, + Money } @doc """ @@ -71,17 +71,17 @@ defmodule Gringotts.Gateways.Stripe do 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. - + Stripe returns an `charge_id` which should be stored at your side and can be used later to: * `capture/3` an amount. * `void/2` a pre-authorization. - + ## Note Uncaptured charges expire in 7 days. For more information, [see authorizing charges and settling later](https://support.stripe.com/questions/can-i-authorize-a-charge-and-then-wait-to-settle-it-later). ## Example The following session shows how one would (pre) authorize a payment of $10 on a sample `card`. - + iex> card = %CreditCard{ first_name: "John", last_name: "Smith", @@ -104,7 +104,7 @@ defmodule Gringotts.Gateways.Stripe do iex> Gringotts.authorize(Gringotts.Gateways.Stripe, amount, card, opts) """ - @spec authorize(number, CreditCard.t() | String.t(), keyword) :: map + @spec authorize(Money.t(), CreditCard.t() | String.t(), keyword) :: map def authorize(amount, payment, opts \\ []) do params = create_params_for_auth_or_purchase(amount, payment, opts, false) commit(:post, "charges", params, opts) @@ -112,10 +112,10 @@ defmodule Gringotts.Gateways.Stripe do @doc """ Transfers amount from the customer to the merchant. - + Stripe 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 session shows how one would process a payment in one-shot, without (pre) authorization. @@ -142,7 +142,7 @@ defmodule Gringotts.Gateways.Stripe do iex> Gringotts.purchase(Gringotts.Gateways.Stripe, amount, card, opts) """ - @spec purchase(number, CreditCard.t() | String.t(), keyword) :: map + @spec purchase(Money.t(), CreditCard.t() | String.t(), keyword) :: map def purchase(amount, payment, opts \\ []) do params = create_params_for_auth_or_purchase(amount, payment, opts) commit(:post, "charges", params, opts) @@ -168,7 +168,7 @@ defmodule Gringotts.Gateways.Stripe do iex> Gringotts.capture(Gringotts.Gateways.Stripe, id, amount, opts) """ - @spec capture(String.t(), number, keyword) :: map + @spec capture(String.t(), Money.t(), keyword) :: map def capture(id, amount, opts \\ []) do params = optional_params(opts) ++ amount_params(amount) commit(:post, "charges/#{id}/capture", params, opts) @@ -176,7 +176,7 @@ defmodule Gringotts.Gateways.Stripe do @doc """ Voids the referenced payment. - + This method attempts a reversal of the either a previous `purchase/3` or `authorize/3` referenced by `charge_id`. As a consequence, the customer will never see any booking on his @@ -190,7 +190,7 @@ defmodule Gringotts.Gateways.Stripe do ## Voiding a previous purchase Stripe will reverse the payment, by sending all the amount back to the customer. Note that this is not the same as `refund/3`. - + ## Example The following session shows how one would void a previous (pre) authorization. Remember that our `capture/3` example only did a partial @@ -223,7 +223,7 @@ defmodule Gringotts.Gateways.Stripe do iex> Gringotts.refund(Gringotts.Gateways.Stripe, amount, id, opts) """ - @spec refund(number, String.t(), keyword) :: map + @spec refund(Money.t(), String.t(), keyword) :: map def refund(amount, id, opts \\ []) do params = optional_params(opts) ++ amount_params(amount) commit(:post, "charges/#{id}/refund", params, opts) @@ -231,7 +231,7 @@ defmodule Gringotts.Gateways.Stripe do @doc """ Stores the payment-source data for later use. - + Stripe can store the payment-source details, for example card which can be used to effectively to process One-Click and Recurring_ payments, and return a `customer_id` for reference. @@ -285,27 +285,22 @@ defmodule Gringotts.Gateways.Stripe do # Private methods defp create_params_for_auth_or_purchase(amount, payment, opts, capture \\ true) do - params = optional_params(opts) - ++ [capture: capture] - ++ amount_params(amount) - ++ source_params(payment, opts) - - params - |> Keyword.has_key?(:currency) - |> with_currency(params, opts[:config]) + [capture: capture] ++ + optional_params(opts) ++ amount_params(amount) ++ source_params(payment, opts) end - def with_currency(true, params, _), do: params - def with_currency(false, params, config), do: [{:currency, config[:default_currency]} | params] - defp create_card_token(params, opts) do commit(:post, "tokens", params, opts) end - defp amount_params(amount), do: [amount: money_to_cents(amount)] + defp amount_params(amount) do + {currency, int_value, _} = Money.to_integer(amount) + [amount: int_value, currency: currency] + end defp source_params(token_or_customer, _) when is_binary(token_or_customer) do [head, _] = String.split(token_or_customer, "_") + case head do "tok" -> [source: token_or_customer] "cus" -> [customer: token_or_customer] @@ -313,64 +308,68 @@ defmodule Gringotts.Gateways.Stripe do end defp source_params(%CreditCard{} = card, opts) do - params = - card_params(card) ++ - address_params(opts[:address]) + params = card_params(card) ++ address_params(opts[:address]) response = create_card_token(params, opts) - case Map.has_key?(response, "error") do - true -> [] - false -> response - |> Map.get("id") - |> source_params(opts) + if Map.has_key?(response, "error") do + [] + else + response + |> Map.get("id") + |> source_params(opts) end end defp source_params(_, _), do: [] defp card_params(%CreditCard{} = card) do - [ "card[name]": CreditCard.full_name(card), + [ + "card[name]": CreditCard.full_name(card), "card[number]": card.number, "card[exp_year]": card.year, "card[exp_month]": card.month, "card[cvc]": card.verification_code - ] + ] end defp card_params(_), do: [] defp address_params(%Address{} = address) do - [ "card[address_line1]": address.street1, + [ + "card[address_line1]": address.street1, "card[address_line2]": address.street2, - "card[address_city]": address.city, + "card[address_city]": address.city, "card[address_state]": address.region, - "card[address_zip]": address.postal_code, + "card[address_zip]": address.postal_code, "card[address_country]": address.country ] end defp address_params(_), do: [] - defp commit(method, path, params \\ [], opts \\ []) do + defp commit(method, path, params, opts) do auth_token = "Bearer " <> opts[:config][:secret_key] - headers = [{"Content-Type", "application/x-www-form-urlencoded"}, {"Authorization", auth_token}] - data = params_to_string(params) - response = HTTPoison.request(method, "#{@base_url}/#{path}", data, headers) + + headers = [ + {"Content-Type", "application/x-www-form-urlencoded"}, + {"Authorization", auth_token} + ] + + response = HTTPoison.request(method, "#{@base_url}/#{path}", {:form, params}, headers) format_response(response) end defp optional_params(opts) do opts - |> Keyword.delete(:config) - |> Keyword.delete(:address) + |> Keyword.delete(:config) + |> Keyword.delete(:address) end defp format_response(response) do case response do - {:ok, %HTTPoison.Response{body: body}} -> body |> Poison.decode! + {:ok, %HTTPoison.Response{body: body}} -> body |> Poison.decode!() _ -> %{"error" => "something went wrong, please try again later"} end end - end diff --git a/test/gateways/monei_test.exs b/test/gateways/monei_test.exs index 3aaa88a1..ae41e9a0 100644 --- a/test/gateways/monei_test.exs +++ b/test/gateways/monei_test.exs @@ -233,12 +233,12 @@ defmodule Gringotts.Gateways.MoneiTest do Bypass.expect_once(bypass, "POST", "/v1/registrations", fn conn -> p_conn = parse(conn) params = p_conn.body_params - params["card.cvv"] == "123" - params["card.expiryMonth"] == "12" - params["card.expiryYear"] == "2099" - params["card.holder"] == "Harry Potter" - params["card.number"] == "4200000000000000" - params["paymentBrand"] == "VISA" + assert params["card.cvv"] == "123" + assert params["card.expiryMonth"] == "12" + assert params["card.expiryYear"] == "2099" + assert params["card.holder"] == "Harry Potter" + assert params["card.number"] == "4200000000000000" + assert params["paymentBrand"] == "VISA" Plug.Conn.resp(conn, 200, @store_success) end) diff --git a/test/gateways/stripe_test.exs b/test/gateways/stripe_test.exs deleted file mode 100644 index 73bc211a..00000000 --- a/test/gateways/stripe_test.exs +++ /dev/null @@ -1,58 +0,0 @@ -defmodule Gringotts.Gateways.StripeTest do - - use ExUnit.Case - - alias Gringotts.Gateways.Stripe - alias Gringotts.{ - CreditCard, - Address - } - - @card %CreditCard{ - first_name: "John", - last_name: "Smith", - number: "4242424242424242", - year: "2017", - month: "12", - verification_code: "123" - } - - @address %Address{ - street1: "123 Main", - street2: "Suite 100", - city: "New York", - region: "NY", - country: "US", - postal_code: "11111" - } - - @required_opts [config: [api_key: "sk_test_vIX41hayC0BKrPWQerLuOMld"], currency: "usd"] - @optional_opts [address: @address] - - describe "authorize/3" do - # test "should authorize wth card and required opts attrs" do - # amount = 5 - # response = Stripe.authorize(amount, @card, @required_opts ++ @optional_opts) - - # assert Map.has_key?(response, "id") - # assert response["amount"] == 500 - # assert response["captured"] == false - # assert response["currency"] == "usd" - # end - - # test "should not authorize if card is not passed" do - # amount = 5 - # response = Stripe.authorize(amount, %{}, @required_opts ++ @optional_opts) - - # assert Map.has_key?(response, "error") - # end - - # test "should not authorize if required opts not present" do - # amount = 5 - # response = Stripe.authorize(amount, @card, @optional_opts) - - # assert Map.has_key?(response, "error") - # end - - end -end diff --git a/test/integration/gateways/stripe_test.exs b/test/integration/gateways/stripe_test.exs new file mode 100644 index 00000000..b390f989 --- /dev/null +++ b/test/integration/gateways/stripe_test.exs @@ -0,0 +1,44 @@ +defmodule Gringotts.Gateways.StripeTest do + + use ExUnit.Case + + alias Gringotts.Gateways.Stripe + alias Gringotts.{ + CreditCard, + Address + } + + @moduletag integration: true + + @amount Money.new(5, :USD) + @card %CreditCard{ + first_name: "John", + last_name: "Smith", + number: "4242424242424242", + year: "2068", # Can't be more than 50 years in the future, Haha. + month: "12", + verification_code: "123" + } + + @address %Address{ + street1: "123 Main", + street2: "Suite 100", + city: "New York", + region: "NY", + country: "US", + postal_code: "11111" + } + + @required_opts [config: [secret_key: "sk_test_vIX41hayC0BKrPWQerLuOMld"]] + @optional_opts [address: @address] + + describe "authorize/3" do + test "with correct params" do + response = Stripe.authorize(@amount, @card, @required_opts ++ @optional_opts) + assert Map.has_key?(response, "id") + assert response["amount"] == 500 + assert response["captured"] == false + assert response["currency"] == "usd" + end + end +end diff --git a/test/integration/money.exs b/test/integration/money.exs index ca42febe..3f5691ba 100644 --- a/test/integration/money.exs +++ b/test/integration/money.exs @@ -26,7 +26,7 @@ defmodule Gringotts.Integration.Gateways.MoneyTest do test "to_integer" do assert match? {"EUR", 4200, -2}, MoneyProtocol.to_integer(@ex_money) - assert match? {"BHD", 42000, -3}, MoneyProtocol.to_integer(@ex_money_bhd) + assert match? {"BHD", 42_000, -3}, MoneyProtocol.to_integer(@ex_money_bhd) end test "to_string" do From 152e90024c55717345e8c6eda836ff515680f06c Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Fri, 9 Feb 2018 14:03:24 +0530 Subject: [PATCH 12/24] Adapted Trexle for new `Response.t` * Trexle does not seem to provide fraud risk, AVS, CVV validation results. There are no docs for this. --- lib/gringotts/gateways/trexle.ex | 12 ++++++-- test/gateways/trexle_test.exs | 52 ++++++++++++-------------------- 2 files changed, 28 insertions(+), 36 deletions(-) diff --git a/lib/gringotts/gateways/trexle.ex b/lib/gringotts/gateways/trexle.ex index 523d4c2f..a9c3d988 100644 --- a/lib/gringotts/gateways/trexle.ex +++ b/lib/gringotts/gateways/trexle.ex @@ -331,17 +331,23 @@ defmodule Gringotts.Gateways.Trexle do { :ok, - Response.success(authorization: token, message: message, raw: results, status_code: code) + %Response{id: token, message: message, raw: body, status_code: code} } end defp respond({:ok, %{status_code: status_code, body: body}}) do {:ok, results} = decode(body) detail = results["detail"] - {:error, Response.error(status_code: status_code, message: detail, raw: results)} + {:error, %Response{status_code: status_code, message: detail, reason: detail, raw: body}} end defp respond({:error, %HTTPoison.Error{} = error}) do - {:error, Response.error(code: error.id, message: "HTTPoison says '#{error.reason}'")} + { + :error, + %Response{ + reason: "network related failure", + message: "HTTPoison says '#{error.reason}' [ID: #{error.id || "nil"}]" + } + } end end diff --git a/test/gateways/trexle_test.exs b/test/gateways/trexle_test.exs index f8a50562..7078b9f6 100644 --- a/test/gateways/trexle_test.exs +++ b/test/gateways/trexle_test.exs @@ -14,8 +14,8 @@ defmodule Gringotts.Gateways.TrexleTest do @valid_card %CreditCard{ first_name: "Harry", last_name: "Potter", - number: "4200000000000000", - year: 2099, + number: "4000056655665556", + year: 2068, month: 12, verification_code: "123", brand: "VISA" @@ -24,7 +24,7 @@ defmodule Gringotts.Gateways.TrexleTest do @invalid_card %CreditCard{ first_name: "Harry", last_name: "Potter", - number: "4200000000000000", + number: "4000056655665556", year: 2010, month: 12, verification_code: "123", @@ -46,10 +46,10 @@ defmodule Gringotts.Gateways.TrexleTest do # 50 US cents, trexle does not work with amount smaller than 50 cents. @bad_amount Money.new("0.49", :USD) - @valid_token "7214344252e11af79c0b9e7b4f3f6234" - @invalid_token "14a62fff80f24a25f775eeb33624bbb3" + @valid_token "some_valid_token" + @invalid_token "some_invalid_token" - @auth %{api_key: "7214344252e11af79c0b9e7b4f3f6234"} + @auth %{api_key: "some_api_key"} @opts [ config: @auth, email: "masterofdeath@ministryofmagic.gov", @@ -64,10 +64,7 @@ defmodule Gringotts.Gateways.TrexleTest do request: fn _method, _url, _body, _headers, _options -> MockResponse.test_for_purchase_with_valid_card() end do - {:ok, response} = Trexle.purchase(@amount, @valid_card, @opts) - assert response.status_code == 201 - assert response.raw["response"]["success"] == true - assert response.raw["response"]["captured"] == false + assert {:ok, response} = Trexle.purchase(@amount, @valid_card, @opts) end end @@ -76,10 +73,8 @@ defmodule Gringotts.Gateways.TrexleTest do request: fn _method, _url, _body, _headers, _options -> MockResponse.test_for_purchase_with_invalid_card() end do - {:error, response} = Trexle.purchase(@amount, @invalid_card, @opts) - assert response.status_code == 400 - assert response.success == false - assert response.message == "Your card's expiration year is invalid." + assert {:error, response} = Trexle.purchase(@amount, @invalid_card, @opts) + assert response.reason == "Your card's expiration year is invalid." end end @@ -88,10 +83,9 @@ defmodule Gringotts.Gateways.TrexleTest do request: fn _method, _url, _body, _headers, _options -> MockResponse.test_for_purchase_with_invalid_amount() end do - {:error, response} = Trexle.purchase(@bad_amount, @valid_card, @opts) + assert {:error, response} = Trexle.purchase(@bad_amount, @valid_card, @opts) assert response.status_code == 400 - assert response.success == false - assert response.message == "Amount must be at least 50 cents" + assert response.reason == "Amount must be at least 50 cents" end end end @@ -102,10 +96,8 @@ defmodule Gringotts.Gateways.TrexleTest do request: fn _method, _url, _body, _headers, _options -> MockResponse.test_for_authorize_with_valid_card() end do - {:ok, response} = Trexle.authorize(@amount, @valid_card, @opts) + assert {:ok, response} = Trexle.authorize(@amount, @valid_card, @opts) assert response.status_code == 201 - assert response.raw["response"]["success"] == true - assert response.raw["response"]["captured"] == false end end end @@ -116,10 +108,8 @@ defmodule Gringotts.Gateways.TrexleTest do request: fn _method, _url, _body, _headers, _options -> MockResponse.test_for_authorize_with_valid_card() end do - {:ok, response} = Trexle.refund(@amount, @valid_token, @opts) + assert {:ok, response} = Trexle.refund(@amount, @valid_token, @opts) assert response.status_code == 201 - assert response.raw["response"]["success"] == true - assert response.raw["response"]["captured"] == false end end end @@ -130,11 +120,9 @@ defmodule Gringotts.Gateways.TrexleTest do request: fn _method, _url, _body, _headers, _options -> MockResponse.test_for_capture_with_valid_chargetoken() end do - {:ok, response} = Trexle.capture(@valid_token, @amount, @opts) + assert {:ok, response} = Trexle.capture(@valid_token, @amount, @opts) + # Why 200 here?? It's 201 everywhere lese. Check trexle docs. assert response.status_code == 200 - assert response.raw["response"]["success"] == true - assert response.raw["response"]["captured"] == true - assert response.message == "Transaction approved" end end @@ -143,10 +131,9 @@ defmodule Gringotts.Gateways.TrexleTest do request: fn _method, _url, _body, _headers, _options -> MockResponse.test_for_capture_with_invalid_chargetoken() end do - {:error, response} = Trexle.capture(@invalid_token, @amount, @opts) + assert {:error, response} = Trexle.capture(@invalid_token, @amount, @opts) assert response.status_code == 400 - assert response.success == false - assert response.message == "invalid token" + assert response.reason == "invalid token" end end end @@ -157,7 +144,7 @@ defmodule Gringotts.Gateways.TrexleTest do request: fn _method, _url, _body, _headers, _options -> MockResponse.test_for_store_with_valid_card() end do - {:ok, response} = Trexle.store(@valid_card, @opts) + assert {:ok, response} = Trexle.store(@valid_card, @opts) assert response.status_code == 201 end end @@ -170,8 +157,7 @@ defmodule Gringotts.Gateways.TrexleTest do MockResponse.test_for_network_failure() end do {:error, response} = Trexle.authorize(@amount, @valid_card, @opts) - assert response.success == false - assert response.message == "HTTPoison says 'some_hackney_error'" + assert response.message == "HTTPoison says 'some_hackney_error' [ID: some_hackney_error_id]" end end end From 6285c7507f0abf748a2fed405039592e56ae0823 Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Wed, 21 Mar 2018 17:25:51 +0530 Subject: [PATCH 13/24] [CAMS] Adapt for new Response.t (#120) * Refactored ResponseHandler, updated Response.t * CAMS now parses AVS and CVV response - that was missing till now. * Removes unnecessary `parse` clause. * Mock tests shouldn't use gringotts.ex --- lib/gringotts/gateways/cams.ex | 151 ++++++++++++++++++--------------- test/gateways/cams_test.exs | 93 +++++++++----------- 2 files changed, 123 insertions(+), 121 deletions(-) diff --git a/lib/gringotts/gateways/cams.ex b/lib/gringotts/gateways/cams.ex index 0cb0c4e8..12687f6b 100644 --- a/lib/gringotts/gateways/cams.ex +++ b/lib/gringotts/gateways/cams.ex @@ -144,7 +144,7 @@ defmodule Gringotts.Gateways.Cams do ## Optional Fields options[ order_id: String, - description: String + description: String ] ## Examples @@ -163,7 +163,7 @@ defmodule Gringotts.Gateways.Cams do iex> {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.Cams, money, card) ``` """ - @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} + @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response.t()} def authorize(money, %CreditCard{} = card, options) do params = [] @@ -210,7 +210,7 @@ defmodule Gringotts.Gateways.Cams do iex> {:ok, capture_result} = Gringotts.capture(Gringotts.Gateways.Cams, money, authorization) ``` """ - @spec capture(Money.t(), String.t(), keyword) :: {:ok | :error, Response} + @spec capture(Money.t(), String.t(), keyword) :: {:ok | :error, Response.t()} def capture(money, transaction_id, options) do params = [transactionid: transaction_id] @@ -246,7 +246,7 @@ defmodule Gringotts.Gateways.Cams do iex> Gringotts.purchase(Gringotts.Gateways.Cams, money, card) ``` """ - @spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} + @spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response.t()} def purchase(money, %CreditCard{} = card, options) do params = [] @@ -278,7 +278,7 @@ defmodule Gringotts.Gateways.Cams do iex> Gringotts.refund(Gringotts.Gateways.Cams, money, capture_id) ``` """ - @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response} + @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response.t()} def refund(money, transaction_id, options) do params = [transactionid: transaction_id] @@ -305,7 +305,7 @@ defmodule Gringotts.Gateways.Cams do iex> Gringotts.void(Gringotts.Gateways.Cams, auth_id) ``` """ - @spec void(String.t(), keyword) :: {:ok | :error, Response} + @spec void(String.t(), keyword) :: {:ok | :error, Response.t()} def void(transaction_id, options) do params = [transactionid: transaction_id] commit("void", params, options) @@ -328,7 +328,7 @@ defmodule Gringotts.Gateways.Cams do iex> Gringotts.validate(Gringotts.Gateways.Cams, card) ``` """ - @spec validate(CreditCard.t(), keyword) :: {:ok | :error, Response} + @spec validate(CreditCard.t(), keyword) :: {:ok | :error, Response.t()} def validate(card, options) do params = [] @@ -378,76 +378,93 @@ defmodule Gringotts.Gateways.Cams do @moduledoc false alias Gringotts.Response + # Fetched from CAMS POST API docs. + @avs_code_translator %{ + "X" => {nil, "pass: 9-character numeric ZIP"}, + "Y" => {nil, "pass: 5-character numeric ZIP"}, + "D" => {nil, "pass: 5-character numeric ZIP"}, + "M" => {nil, "pass: 5-character numeric ZIP"}, + "2" => {"pass: customer name", "pass: 5-character numeric ZIP"}, + "6" => {"pass: customer name", "pass: 5-character numeric ZIP"}, + "A" => {"pass: only address", "fail"}, + "B" => {"pass: only address", "fail"}, + "3" => {"pass: address, customer name", "fail"}, + "7" => {"pass: address, customer name", "fail"}, + "W" => {"fail", "pass: 9-character numeric ZIP match"}, + "Z" => {"fail", "pass: 5-character ZIP match"}, + "P" => {"fail", "pass: 5-character ZIP match"}, + "L" => {"fail", "pass: 5-character ZIP match"}, + "1" => {"pass: only customer name", "pass: 5-character ZIP"}, + "5" => {"pass: only customer name", "pass: 5-character ZIP"}, + "N" => {"fail", "fail"}, + "C" => {"fail", "fail"}, + "4" => {"fail", "fail"}, + "8" => {"fail", "fail"}, + "U" => {nil, nil}, + "G" => {nil, nil}, + "I" => {nil, nil}, + "R" => {nil, nil}, + "E" => {nil, nil}, + "S" => {nil, nil}, + "0" => {nil, nil}, + "O" => {nil, nil}, + "" => {nil, nil} + } + + # Fetched from CAMS POST API docs. + @cvc_code_translator %{ + "M" => "pass", + "N" => "fail", + "P" => "not_processed", + "S" => "Merchant indicated that CVV2/CVC2 is not present on card", + "U" => "Issuer is not certified and/or has not provided Visa encryption key" + } + @doc false def parse({:ok, %HTTPoison.Response{body: body, status_code: 200}}) do - body = URI.decode_query(body) - - [status_code: 200] - |> set_authorization(body) - |> set_success(body) - |> set_message(body) - |> set_params(body) - |> set_error_code(body) - |> handle_opts() - end - - def parse({:ok, %HTTPoison.Response{body: body, status_code: 400}}) do - body = URI.decode_query(body) - set_params([status_code: 400], body) + decoded_body = URI.decode_query(body) + {street, zip_code} = @avs_code_translator[decoded_body["avsresponse"]] + gateway_code = decoded_body["response_code"] + message = decoded_body["responsetext"] + response = %Response{ + status_code: 200, + id: decoded_body["transactionid"], + gateway_code: gateway_code, + avs_result: %{street: street, zip_code: zip_code}, + cvc_result: @cvc_code_translator[decoded_body["cvvresponse"]], + message: decoded_body["responsetext"], + raw: body + } + + if successful?(gateway_code) do + {:ok, response} + else + {:error, %{response | reason: message}} + end end - def parse({:ok, %HTTPoison.Response{body: body, status_code: 404}}) do - body = URI.decode_query(body) + def parse({:ok, %HTTPoison.Response{body: body, status_code: code}}) do + response = %Response{ + status_code: code, + raw: body + } - [status_code: 404] - |> handle_not_found(body) - |> handle_opts() + {:error, response} end def parse({:error, %HTTPoison.Error{} = error}) do - [ - message: "HTTPoison says #{error.reason}", - error_code: error.id, - success: false - ] - end - - defp set_authorization(opts, %{"transactionid" => id}) do - opts ++ [authorization: id] - end - - defp set_message(opts, %{"responsetext" => message}) do - opts ++ [message: message] - end - - defp set_params(opts, body) do - opts ++ [params: body] - end - - defp set_error_code(opts, %{"response_code" => response_code}) do - opts ++ [error_code: response_code] - end - - defp set_success(opts, %{"response_code" => response_code}) do - opts ++ [success: response_code == "100"] + { + :error, + %Response{ + reason: "network related failure", + message: "HTTPoison says '#{error.reason}' [ID: #{error.id || "nil"}]", + success: false + } + } end - defp handle_not_found(opts, body) do - error = parse_html(body) - opts ++ [success: false, message: error] - end - - defp parse_html(body) do - error_message = List.to_string(Map.keys(body)) - [_ | parse_message] = Regex.run(~r|(.*)|, error_message) - List.to_string(parse_message) - end - - defp handle_opts(opts) do - case Keyword.fetch(opts, :success) do - {:ok, true} -> {:ok, Response.success(opts)} - {:ok, false} -> {:ok, Response.error(opts)} - end + defp successful?(gateway_code) do + gateway_code == "100" end end end diff --git a/test/gateways/cams_test.exs b/test/gateways/cams_test.exs index 6faf56b2..edf0e5f4 100644 --- a/test/gateways/cams_test.exs +++ b/test/gateways/cams_test.exs @@ -43,9 +43,10 @@ defmodule Gringotts.Gateways.CamsTest do } @auth %{username: "some_secret_user_name", password: "some_secret_password"} @options [ - order_id: 0001, + order_id: 1, billing_address: @address, - description: "Store Purchase" + description: "Store Purchase", + config: @auth ] @money Money.new(:USD, 100) @@ -53,40 +54,32 @@ defmodule Gringotts.Gateways.CamsTest do @money_less Money.new(:USD, 99) @bad_currency Money.new(:INR, 100) - @authorization "some_transaction_id" - @bad_authorization "some_fake_transaction_id" - - setup_all do - Application.put_env(:gringotts, Gateway, - username: "some_secret_user_name", - password: "some_secret_password" - ) - end + @id "some_transaction_id" + @bad_id "some_fake_transaction_id" describe "purchase" do test "with correct params" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_purchase() end do - {:ok, %Response{success: result}} = Gringotts.purchase(Gateway, @money, @card, @options) - assert result + assert {:ok, %Response{}} = Gateway.purchase(@money, @card, @options) end end test "with bad card" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.failed_purchase_with_bad_credit_card() end do - {:ok, %Response{message: result}} = - Gringotts.purchase(Gateway, @money, @bad_card, @options) + {:error, %Response{reason: reason}} = + Gateway.purchase(@money, @bad_card, @options) - assert String.contains?(result, "Invalid Credit Card Number") + assert String.contains?(reason, "Invalid Credit Card Number") end end test "with invalid currency" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.with_invalid_currency() end do - {:ok, %Response{message: result}} = Gringotts.purchase(Gateway, @bad_currency, @card, @options) - assert String.contains?(result, "The cc payment type") + {:error, %Response{reason: reason}} = Gateway.purchase(@bad_currency, @card, @options) + assert String.contains?(reason, "The cc payment type") end end end @@ -95,18 +88,17 @@ defmodule Gringotts.Gateways.CamsTest do test "with correct params" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_authorize() end do - {:ok, %Response{success: result}} = Gringotts.authorize(Gateway, @money, @card, @options) - assert result + assert {:ok, %Response{}} = Gateway.authorize(@money, @card, @options) end end test "with bad card" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.failed_authorized_with_bad_card() end do - {:ok, %Response{message: result}} = - Gringotts.authorize(Gateway, @money, @bad_card, @options) + {:error, %Response{reason: reason}} = + Gateway.authorize(@money, @bad_card, @options) - assert String.contains?(result, "Invalid Credit Card Number") + assert String.contains?(reason, "Invalid Credit Card Number") end end end @@ -114,49 +106,45 @@ defmodule Gringotts.Gateways.CamsTest do describe "capture" do test "with full amount" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_capture() end do - {:ok, %Response{success: result}} = - Gringotts.capture(Gateway, @money, @authorization, @options) - - assert result + assert {:ok, %Response{}} = + Gateway.capture(@money, @id , @options) end end test "with partial amount" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_capture() end do - {:ok, %Response{success: result}} = - Gringotts.capture(Gateway, @money_less, @authorization, @options) - - assert result + assert {:ok, %Response{}} = + Gateway.capture(@money_less, @id , @options) end end test "with invalid transaction_id" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.invalid_transaction_id() end do - {:ok, %Response{message: result}} = - Gringotts.capture(Gateway, @money, @bad_authorization, @options) + {:error, %Response{reason: reason}} = + Gateway.capture(@money, @bad_id, @options) - assert String.contains?(result, "Transaction not found") + assert String.contains?(reason, "Transaction not found") end end - test "with more than authorization amount" do + test "with more than authorized amount" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.more_than_authorization_amount() end do - {:ok, %Response{message: result}} = - Gringotts.capture(Gateway, @money_more, @authorization, @options) + {:error, %Response{reason: reason}} = + Gateway.capture(@money_more, @id , @options) - assert String.contains?(result, "exceeds the authorization amount") + assert String.contains?(reason, "exceeds the authorization amount") end end test "on already captured transaction" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.multiple_capture_on_same_transaction() end do - {:ok, %Response{message: result}} = - Gringotts.capture(Gateway, @money, @authorization, @options) + {:error, %Response{reason: reason}} = + Gateway.capture(@money, @id , @options) - assert String.contains?(result, "A capture requires that") + assert String.contains?(reason, "A capture requires that") end end end @@ -164,20 +152,18 @@ defmodule Gringotts.Gateways.CamsTest do describe "refund" do test "with correct params" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_refund() end do - {:ok, %Response{success: result}} = - Gringotts.refund(Gateway, @money, @authorization, @options) - - assert result + assert {:ok, %Response{}} = + Gateway.refund(@money, @id , @options) end end test "with more than purchased amount" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.more_than_purchase_amount() end do - {:ok, %Response{message: result}} = - Gringotts.refund(Gateway, @money_more, @authorization, @options) + {:error, %Response{reason: reason}} = + Gateway.refund(@money_more, @id , @options) - assert String.contains?(result, "Refund amount may not exceed") + assert String.contains?(reason, "Refund amount may not exceed") end end end @@ -185,16 +171,16 @@ defmodule Gringotts.Gateways.CamsTest do describe "void" do test "with correct params" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_void() end do - {:ok, %Response{message: result}} = Gringotts.void(Gateway, @authorization, @options) - assert String.contains?(result, "Void Successful") + {:ok, %Response{message: message}} = Gateway.void(@id , @options) + assert String.contains?(message, "Void Successful") end end test "with invalid transaction_id" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.invalid_transaction_id() end do - {:ok, %Response{message: result}} = Gringotts.void(Gateway, @bad_authorization, @options) - assert String.contains?(result, "Transaction not found") + {:error, %Response{reason: reason}} = Gateway.void(@bad_id, @options) + assert String.contains?(reason, "Transaction not found") end end end @@ -203,8 +189,7 @@ defmodule Gringotts.Gateways.CamsTest do test "with correct params" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.validate_creditcard() end do - {:ok, %Response{success: result}} = Gateway.validate(@card, @options ++ [config: @auth]) - assert result + assert {:ok, %Response{}} = Gateway.validate(@card, @options ++ [config: @auth]) end end end From 494a5a337a2969349a48514116a9069a34a22964 Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Thu, 22 Mar 2018 12:42:27 +0530 Subject: [PATCH 14/24] Format project and migrate to CodeCov (#135) * Update travis config, add .formater.exs * Migrate to CodeCov * Travis will run the formatter check * Add git-hooks and update contribution guide * Changed credo lin-length config from 80 to 100 * Ran the formatter on the project * Fix credo warnings --- .credo.exs | 2 +- .formatter.exs | 7 + .scripts/inch_report.sh | 20 + .scripts/post-commit | 12 + .scripts/pre-commit | 50 ++ .travis.yml | 26 +- CONTRIBUTING.md | 83 ++- lib/gringotts.ex | 54 +- lib/gringotts/adapter.ex | 15 +- lib/gringotts/address.ex | 2 + lib/gringotts/credit_card.ex | 22 +- lib/gringotts/gateways/authorize_net.ex | 80 ++- lib/gringotts/gateways/base.ex | 14 +- lib/gringotts/gateways/bogus.ex | 28 +- lib/gringotts/gateways/cams.ex | 3 +- lib/gringotts/gateways/global_collect.ex | 60 +- lib/gringotts/gateways/paymill.ex | 263 ++++----- lib/gringotts/gateways/wire_card.ex | 193 ++++--- lib/gringotts/response.ex | 39 +- mix.exs | 18 +- test/gateways/authorize_net_test.exs | 37 +- test/gateways/bogus_test.exs | 21 +- test/gateways/cams_test.exs | 29 +- test/gateways/global_collect_test.exs | 84 ++- test/gateways/monei_test.exs | 32 +- test/gateways/trexle_test.exs | 4 +- test/gateways/wire_card_test.exs | 12 +- test/gringotts_test.exs | 6 +- test/integration/gateways/monei_test.exs | 17 +- test/integration/gateways/stripe_test.exs | 7 +- test/integration/money.exs | 32 +- test/mocks/authorize_net_mock.exs | 673 +++++++++++++--------- test/mocks/cams_mock.exs | 350 +++++------ test/mocks/global_collect_mock.exs | 255 ++++---- test/mocks/trexle_mock.exs | 340 +++++------ 35 files changed, 1623 insertions(+), 1267 deletions(-) create mode 100644 .formatter.exs create mode 100644 .scripts/inch_report.sh create mode 100755 .scripts/post-commit create mode 100755 .scripts/pre-commit diff --git a/.credo.exs b/.credo.exs index df92ae85..9381d3f7 100644 --- a/.credo.exs +++ b/.credo.exs @@ -77,7 +77,7 @@ {Credo.Check.Readability.FunctionNames}, {Credo.Check.Readability.LargeNumbers}, - {Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 80}, + {Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 100}, {Credo.Check.Readability.ModuleAttributeNames}, {Credo.Check.Readability.ModuleDoc}, {Credo.Check.Readability.ModuleNames}, diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 00000000..aa758aff --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,7 @@ +[ + inputs: [ + "{lib,config}/**/*.{ex,exs}", # lib and config + "test/**/*.{ex,exs}", # tests + "mix.exs" + ] +] diff --git a/.scripts/inch_report.sh b/.scripts/inch_report.sh new file mode 100644 index 00000000..a352261d --- /dev/null +++ b/.scripts/inch_report.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +set -e +bold=$(tput bold) +purple='\e[106m' +normal=$(tput sgr0) +allowed_branches="^(master)|(develop)$" + +echo -e "${bold}${purple}" +if [ $TRAVIS_PULL_REQUEST = false ]; then + if [[ $TRAVIS_BRANCH =~ $allowed_branches ]]; then + env MIX_ENV=docs mix deps.get + env MIX_ENV=docs mix inch.report + else + echo "Skipping Inch CI report because this branch does not match on /$allowed_branches/" + fi +else + echo "Skipping Inch CI report because this is a PR build" +fi +echo -e "${normal}" diff --git a/.scripts/post-commit b/.scripts/post-commit new file mode 100755 index 00000000..ada60194 --- /dev/null +++ b/.scripts/post-commit @@ -0,0 +1,12 @@ +#!/bin/sh +# +# Runs credo and the formatter on the staged files, after the commit is made +# This is purely for notification and will not halt/change your commit. + +RED='\033[1;31m' +LGRAY='\033[1;30m' +NC='\033[0m' # No Color + +printf "${RED}Running 'mix credo --strict --format=oneline' on project...${NC}\n" +mix credo --strict --format=oneline +echo diff --git a/.scripts/pre-commit b/.scripts/pre-commit new file mode 100755 index 00000000..1f473c8c --- /dev/null +++ b/.scripts/pre-commit @@ -0,0 +1,50 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git commit" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message if +# it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-commit". + +if git rev-parse --verify HEAD >/dev/null 2>&1 +then + against=HEAD +else + # Initial commit: diff against an empty tree object + against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 +fi + +# If you want to allow non-ASCII filenames set this variable to true. +allownonascii=$(git config --bool hooks.allownonascii) + +# Redirect output to stderr. +exec 1>&2 + +# Cross platform projects tend to avoid non-ASCII filenames; prevent +# them from being added to the repository. We exploit the fact that the +# printable range starts at the space character and ends with tilde. +if [ "$allownonascii" != "true" ] && + # Note that the use of brackets around a tr range is ok here, (it's + # even required, for portability to Solaris 10's /usr/bin/tr), since + # the square bracket bytes happen to fall in the designated range. + test $(git diff --cached --name-only --diff-filter=A -z $against | + LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 +then + cat <<\EOF +Error: Attempt to add a non-ASCII file name. + +This can cause problems if you want to work with people on other platforms. + +To be portable it is advisable to rename the file. + +If you know what you are doing you can disable this check using: + + git config hooks.allownonascii true +EOF + exit 1 +fi + +# Also run the mix format task, just check though. +exec mix format --check-formatted + diff --git a/.travis.yml b/.travis.yml index 82c3f5a9..a238ae93 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,28 @@ language: elixir -elixir: - - 1.5.2 + otp_release: - - 20.1 + - 20.2 before_install: - mix local.hex --force - mix local.rebar --force - mix deps.get script: - - mix coveralls.travis --include integration + - set -e + - MIX_ENV=test mix format --check-formatted + - set +e + - mix coveralls.json --include=integration after_script: - - MIX_ENV=docs mix deps.get - - MIX_ENV=docs mix inch.report + - bash <(curl -s https://codecov.io/bash) + - bash .scripts/inch_report.sh + +matrix: + include: + - elixir: "1.5.3" + script: + - mix coveralls.json --include=integration + - elixir: "1.6.2" + +notifications: + email: + recipients: + - ananya95+travis@gmail.com diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6eeb7033..f096ca9d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,40 +24,42 @@ processed. [roadmap]: https://github.com/aviabird/gringotts/wiki/Roadmap [wiki-arch]: https://github.com/aviabird/gringotts/wiki/Architecture -### PR submission checklist +# Style Guidelines -Each PR should introduce a *focussed set of changes*, and ideally not span over -unrelated modules. +We follow +[lexmag/elixir-style-guide](https://github.com/lexmag/elixir-style-guide) and +[rrrene/elixir-style-guide](https://github.com/rrrene/elixir-style-guide) (both +overlap a lot), and use the elixir formatter. -* [ ] Run the edited files through [credo][credo] and the Elixir - [formatter][hashrocket-formatter] (new in `v1.6`). -* [ ] Check the test coverage by running `mix coveralls`. 100% coverage is not - strictly required. -* [ ] If the PR introduces a new Gateway or just Gateway specific changes, - please format the title like so,\ - `[] ` +To enforce these, and also to make it easier for new contributors to adhere to +our style, we've provided a collection of handy `git-hooks` under the `.scripts/` +directory. -[gringotts]: https://github.com/aviabird/gringotts -[milestones]: https://github.com/aviabird/gringotts/milestones -[issues]: https://github.com/aviabird/gringotts/issues -[first-issues]: https://github.com/aviabird/gringotts/issues?q=is%3Aissue+is%3Aopen+label%3A"good+first+issue" -[ch-issues]: https://github.com/aviabird/gringotts/issues?q=is%3Aissue+is%3Aopen+label%3A"hotfix%3A+community-help" -[hexdocs]: https://hexdocs.pm/gringotts -[credo]: https://github.com/rrrene/credo -[hashrocket-formatter]: https://hashrocket.com/blog/posts/format-your-elixir-code-now +* `.scripts/pre-commit` Runs the `format --check-formatted` task. +* `.scripts/post-commit` Runs a customised `credo` check. -# Style Guidelines +While we do not force you to use these hooks, you could write your +very own by taking inspiration from ours :smile: -We use [`credo`][credo] and the elixir formatter for consistent style, so please -use them! +To set the `git-hooks` as provided, go to the repo root, +```sh +cd path/to/gringotts/ +``` +and make these symbolic links: +```sh +ln -s .scripts/pre-commit .git/hooks/pre-commit +ln -s .scripts/post-commit .git/hooks/post-commit +``` + +> Note that our CI will fail your PR if you dont run `mix format` in the project +> root. ## General Rules -* Keep line length below 120 characters. +* Keep line length below 100 characters. * Complex anonymous functions should be extracted into named functions. * One line functions, should only take up one line! -* Pipes are great, but don't use them, if they are less readable than brackets - then drop the pipe! +* Pipes are great, but don't use them if they are less readable than brackets! ## Writing documentation @@ -77,6 +79,39 @@ inspiration. using `mock` as it constrains tests to run serially. Use [`mox`][mox] instead.\ Take a look at [MONEI's mock tests][src-monei-tests] for inspiration. +# PR submission checklist + +Each PR should introduce a *focussed set of changes*, and ideally not span over +unrelated modules. + +* [ ] Format the project with the Elixir formatter. + ```sh + cd path/to/gringotts/ + mix format + ``` +* [ ] Run the edited files through [credo][credo] with the `--strict` flag. + ```sh + cd path/to/gringotts/ + mix credo --strict + ``` +* [ ] Check the test coverage by running `mix coveralls`. 100% coverage is not + strictly required. +* [ ] If the PR introduces a new Gateway or just Gateway specific changes, + please format the title like so,\ + `[] ` + +> **Note** +> You can skip the first two steps if you have set up `git-hooks` as we have +> provided! + +[gringotts]: https://github.com/aviabird/gringotts +[milestones]: https://github.com/aviabird/gringotts/milestones +[issues]: https://github.com/aviabird/gringotts/issues +[first-issues]: https://github.com/aviabird/gringotts/issues?q=is%3Aissue+is%3Aopen+label%3A"good+first+issue" +[ch-issues]: https://github.com/aviabird/gringotts/issues?q=is%3Aissue+is%3Aopen+label%3A"hotfix%3A+community-help" +[hexdocs]: https://hexdocs.pm/gringotts +[credo]: https://github.com/rrrene/credo + -------------------------------------------------------------------------------- > **Where to next?** diff --git a/lib/gringotts.ex b/lib/gringotts.ex index 11bda944..003c6aba 100644 --- a/lib/gringotts.ex +++ b/lib/gringotts.ex @@ -6,21 +6,21 @@ defmodule Gringotts do easy for merchants to use multiple gateways. All gateways must conform to the API as described in this module, but can also support more gateway features than those required by Gringotts. - + ## Standard API arguments All requests to Gringotts are served by a supervised worker, this might be made optional in future releases. - + ### `gateway` (Module) Name - + The `gateway` to which this request is made. This is required in all API calls because Gringotts supports multiple Gateways. #### Example If you've configured Gringotts to work with Stripe, you'll do this to make an `authorization` request: - + Gringotts.authorize(Gingotts.Gateways.Stripe, other args ...) ### `amount` _and currency_ @@ -39,7 +39,7 @@ defmodule Gringotts do Otherwise, just wrap your `amount` with the `currency` together in a `Map` like so, money = %{value: Decimal.new("100.50"), currency: "USD"} - + > When this highly precise `amount` is serialized into the network request, we > use a potentially lossy `Gringotts.Money.to_string/1` or > `Gringotts.Money.to_integer/1` to perform rounding (if required) using the @@ -49,14 +49,14 @@ defmodule Gringotts do > STRONGLY RECOMMEND that merchants perform any required rounding and handle > remainders in their application logic -- before passing the `amount` to > Gringotts's API.** - + #### Example If you use `ex_money` in your project, and want to make an authorization for $2.99 to the `XYZ` Gateway, you'll do the following: # the money lib is aliased as "MoneyLib" - + amount = MoneyLib.new("2.99", :USD) Gringotts.authorize(Gringotts.Gateways.XYZ, amount, some_card, extra_options) @@ -65,7 +65,7 @@ defmodule Gringotts do [money]: https://hexdocs.pm/money/Money.html [iss-money-lib-support]: https://github.com/aviabird/gringotts/projects/3#card-6801146 [wiki-half-even]: https://en.wikipedia.org/wiki/Rounding#Round_half_to_even - + ### `card`, a payment source Gringotts provides a `Gringotts.CreditCard` type to hold card parameters @@ -78,7 +78,7 @@ defmodule Gringotts do gateways might support payment via other instruments such as e-wallets, vouchers, bitcoins or banks. Support for these instruments is planned in future releases. - + %CreditCard { first_name: "Harry", last_name: "Potter", @@ -93,18 +93,18 @@ defmodule Gringotts do `opts` is a `keyword` list of other options/information accepted by the gateway. The format, use and structure is gateway specific and documented in the Gateway's docs. - + ## Configuration - + Merchants must provide Gateway specific configuration in their application config in the usual elixir style. The required and optional fields are documented in every Gateway. - + > The required config keys are validated at runtime, as they include > authentication information. See `Gringotts.Adapter.validate_config/2`. - + ### Global config - + This is set using the `:global_config` key once in your application. #### `:mode` @@ -120,9 +120,9 @@ defmodule Gringotts do environments. * `:prod` -- for live environment, all requests will reach the financial and banking networks. Switch to this in your application's `:prod` environment. - + **Example** - + config :gringotts, :global_config, # for live environment mode: :prod @@ -136,7 +136,7 @@ defmodule Gringotts do # some_documented_key: associated_value # some_other_key: another_value """ - + @doc """ Performs a (pre) Authorize operation. @@ -171,7 +171,7 @@ defmodule Gringotts do * multiple captures, per authorization ## Example - + To capture $4.20 on a previously authorized payment worth $4.20 by referencing the obtained authorization `id` with the `XYZ` gateway, @@ -181,7 +181,7 @@ defmodule Gringotts do card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} Gringotts.capture(Gringotts.Gateways.XYZ, amount, auth_result.id, opts) """ - def capture(gateway, id, amount, opts \\ []) do + def capture(gateway, id, amount, opts \\ []) do config = get_and_validate_config(gateway) gateway.capture(id, amount, [{:config, config} | opts]) end @@ -195,14 +195,14 @@ defmodule Gringotts do This method _can_ be implemented as a chained call to `authorize/3` and `capture/3`. But it must be implemented as a single call to the Gateway if it provides a specific endpoint or action for this. - + > ***Note!** > All gateways must implement (atleast) this method. ## Example To process a purchase worth $4.2, with the `XYZ` gateway, - + amount = Money.new("4.2", :USD) # IF YOU DON'T USE ex_money # amount = %{value: Decimal.new("4.2"), currency: "EUR"} @@ -229,7 +229,7 @@ defmodule Gringotts do # amount = %{value: Decimal.new("4.2"), currency: "EUR"} Gringotts.purchase(Gringotts.Gateways.XYZ, amount, id, opts) """ - def refund(gateway, amount, id, opts \\ []) do + def refund(gateway, amount, id, opts \\ []) do config = get_and_validate_config(gateway) gateway.refund(amount, id, [{:config, config} | opts]) end @@ -238,7 +238,7 @@ defmodule Gringotts do Stores the payment-source data for later use, returns a `token`. > The token must be returned in the `Response.authorization` field. - + ## Note This usually enables _One-Click_ and _Recurring_ payments. @@ -250,7 +250,7 @@ defmodule Gringotts do card = %Gringotts.CreditCard{first_name: "Jo", last_name: "Doe", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} Gringotts.store(Gringotts.Gateways.XYZ, card, opts) """ - def store(gateway, card, opts \\ []) do + def store(gateway, card, opts \\ []) do config = get_and_validate_config(gateway) gateway.store(card, [{:config, config} | opts]) end @@ -264,11 +264,11 @@ defmodule Gringotts do ## Example To unstore with the `XYZ` gateway, - + token = "some_privileged_customer" Gringotts.unstore(Gringotts.Gateways.XYZ, token) """ - def unstore(gateway, token, opts \\ []) do + def unstore(gateway, token, opts \\ []) do config = get_and_validate_config(gateway) gateway.unstore(token, [{:config, config} | opts]) end @@ -289,7 +289,7 @@ defmodule Gringotts do id = "some_previously_obtained_token" Gringotts.void(Gringotts.Gateways.XYZ, id, opts) """ - def void(gateway, id, opts \\ []) do + def void(gateway, id, opts \\ []) do config = get_and_validate_config(gateway) gateway.void(id, [{:config, config} | opts]) end diff --git a/lib/gringotts/adapter.ex b/lib/gringotts/adapter.ex index 978cd1d1..5edd06f2 100644 --- a/lib/gringotts/adapter.ex +++ b/lib/gringotts/adapter.ex @@ -15,7 +15,7 @@ defmodule Gringotts.Adapter do Say a merchant must provide his `secret_user_name` and `secret_password` to some Gateway `XYZ`. Then, `Gringotts` expects that the `GatewayXYZ` module would use `Adapter` in the following manner: - + ``` defmodule Gringotts.Gateways.GatewayXYZ do @@ -38,7 +38,7 @@ defmodule Gringotts.Adapter do ``` """ - + defmacro __using__(opts) do quote bind_quoted: [opts: opts] do @required_config opts[:required_config] || [] @@ -50,16 +50,19 @@ defmodule Gringotts.Adapter do is not available or missing from the Application config. """ def validate_config(config) do - missing_keys = Enum.reduce(@required_config, [], fn(key, missing_keys) -> - if config[key] in [nil, ""], do: [key | missing_keys], else: missing_keys - end) + missing_keys = + Enum.reduce(@required_config, [], fn key, missing_keys -> + if config[key] in [nil, ""], do: [key | missing_keys], else: missing_keys + end) + raise_on_missing_config(missing_keys, config) end defp raise_on_missing_config([], _config), do: :ok + defp raise_on_missing_config(key, config) do raise ArgumentError, """ - expected #{inspect key} to be set, got: #{inspect config} + expected #{inspect(key)} to be set, got: #{inspect(config)} """ end end diff --git a/lib/gringotts/address.ex b/lib/gringotts/address.ex index e1c50c95..57c8dd5c 100644 --- a/lib/gringotts/address.ex +++ b/lib/gringotts/address.ex @@ -1,3 +1,5 @@ defmodule Gringotts.Address do + @moduledoc false + defstruct [:street1, :street2, :city, :region, :country, :postal_code, :phone] end diff --git a/lib/gringotts/credit_card.ex b/lib/gringotts/credit_card.ex index 01811e00..e98481a0 100644 --- a/lib/gringotts/credit_card.ex +++ b/lib/gringotts/credit_card.ex @@ -4,6 +4,7 @@ defmodule Gringotts.CreditCard do """ defstruct [:number, :month, :year, :first_name, :last_name, :verification_code, :brand] + @typedoc """ Represents a Credit Card. @@ -29,23 +30,24 @@ defmodule Gringotts.CreditCard do [mo]: http://www.maestrocard.com/gateway/index.html [dc]: http://www.dinersclub.com/ """ - @type t :: %__MODULE__{number: String.t, - month: 1..12, - year: non_neg_integer, - first_name: String.t, - last_name: String.t, - verification_code: String.t, - brand: String.t} + @type t :: %__MODULE__{ + number: String.t(), + month: 1..12, + year: non_neg_integer, + first_name: String.t(), + last_name: String.t(), + verification_code: String.t(), + brand: String.t() + } @doc """ Returns the full name of the card holder. Joins `first_name` and `last_name` with a space in between. """ - @spec full_name(t) :: String.t + @spec full_name(t) :: String.t() def full_name(card) do name = "#{card.first_name} #{card.last_name}" - String.trim(name) + String.trim(name) end - end diff --git a/lib/gringotts/gateways/authorize_net.ex b/lib/gringotts/gateways/authorize_net.ex index 59281314..2612bb7a 100644 --- a/lib/gringotts/gateways/authorize_net.ex +++ b/lib/gringotts/gateways/authorize_net.ex @@ -89,7 +89,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do that gives you a pre-configured sample app ready-to-go. + You could use the same config or update it the with your "secrets" [above](#module-configuring-your-authorizenet-account-at-gringotts). - + 2. To save a lot of time, create a [`.iex.exs`][iex-docs] file as shown in [this gist][authorize_net.iex.exs] to introduce a set of handy bindings and aliases. @@ -226,7 +226,6 @@ defmodule Gringotts.Gateways.AuthorizeNet do customer_ip: String ] - ## Example iex> amount = Money.new(20, :USD) iex> opts = [ @@ -749,21 +748,36 @@ defmodule Gringotts.Gateways.AuthorizeNet do ] @avs_code_translator %{ - "A" => {"pass", "fail"}, #The street address matched, but the postal code did not. - "B" => {nil, nil}, # No address information was provided. - "E" => {"fail", nil}, # The AVS check returned an error. - "G" => {nil, nil}, # The card was issued by a bank outside the U.S. and does not support AVS. - "N" => {"fail", "fail"}, # Neither the street address nor postal code matched. - "P" => {nil, nil}, # AVS is not applicable for this transaction. - "R" => {nil, nil}, # Retry — AVS was unavailable or timed out. - "S" => {nil, nil}, # AVS is not supported by card issuer. - "U" => {nil, nil}, # Address information is unavailable. - "W" => {"fail", "pass"}, # The US ZIP+4 code matches, but the street address does not. - "X" => {"pass", "pass"}, # Both the street address and the US ZIP+4 code matched. - "Y" => {"pass", "pass"}, # The street address and postal code matched. - "Z" => {"fail", "pass"}, # The postal code matched, but the street address did not. - "" => {nil, nil}, # fallback in-case of absence - nil => {nil, nil} # fallback in-case of absence + # The street address matched, but the postal code did not. + "A" => {"pass", "fail"}, + # No address information was provided. + "B" => {nil, nil}, + # The AVS check returned an error. + "E" => {"fail", nil}, + # The card was issued by a bank outside the U.S. and does not support AVS. + "G" => {nil, nil}, + # Neither the street address nor postal code matched. + "N" => {"fail", "fail"}, + # AVS is not applicable for this transaction. + "P" => {nil, nil}, + # Retry — AVS was unavailable or timed out. + "R" => {nil, nil}, + # AVS is not supported by card issuer. + "S" => {nil, nil}, + # Address information is unavailable. + "U" => {nil, nil}, + # The US ZIP+4 code matches, but the street address does not. + "W" => {"fail", "pass"}, + # Both the street address and the US ZIP+4 code matched. + "X" => {"pass", "pass"}, + # The street address and postal code matched. + "Y" => {"pass", "pass"}, + # The postal code matched, but the street address did not. + "Z" => {"fail", "pass"}, + # fallback in-case of absence + "" => {nil, nil}, + # fallback in-case of absence + nil => {nil, nil} } @cvc_code_translator %{ @@ -772,7 +786,8 @@ defmodule Gringotts.Gateways.AuthorizeNet do "P" => "CVV was not processed.", "S" => "CVV should have been present but was not indicated.", "U" => "The issuer was unable to process the CVV check.", - nil => nil # fallback in-case of absence + # fallback in-case of absence + nil => nil } @cavv_code_translator %{ @@ -784,16 +799,22 @@ defmodule Gringotts.Gateways.AuthorizeNet do "4" => "CAVV validation could not be performed; issuer system error.", "5" => "Reserved for future use.", "6" => "Reserved for future use.", - "7" => "CAVV failed validation, but the issuer is available. Valid for U.S.-issued card submitted to non-U.S acquirer.", - "8" => "CAVV passed validation and the issuer is available. Valid for U.S.-issued card submitted to non-U.S. acquirer.", - "9" => "CAVV failed validation and the issuer is unavailable. Valid for U.S.-issued card submitted to non-U.S acquirer.", - "A" => "CAVV passed validation but the issuer unavailable. Valid for U.S.-issued card submitted to non-U.S acquirer.", + "7" => + "CAVV failed validation, but the issuer is available. Valid for U.S.-issued card submitted to non-U.S acquirer.", + "8" => + "CAVV passed validation and the issuer is available. Valid for U.S.-issued card submitted to non-U.S. acquirer.", + "9" => + "CAVV failed validation and the issuer is unavailable. Valid for U.S.-issued card submitted to non-U.S acquirer.", + "A" => + "CAVV passed validation but the issuer unavailable. Valid for U.S.-issued card submitted to non-U.S acquirer.", "B" => "CAVV passed validation, information only, no liability shift.", - nil => nil # fallback in-case of absence + # fallback in-case of absence + nil => nil } def respond(body) do response_map = XmlToMap.naive_map(body) + case extract_gateway_response(response_map) do :undefined_response -> { @@ -812,11 +833,11 @@ defmodule Gringotts.Gateways.AuthorizeNet do def extract_gateway_response(response_map) do # The type of the response should be supported - @supported_response_types - |> Stream.map(&Map.get(response_map, &1, nil)) # Find the first non-nil from the above, if all are `nil`... # We are in trouble! - |> Enum.find(:undefined_response, &(&1)) + @supported_response_types + |> Stream.map(&Map.get(response_map, &1, nil)) + |> Enum.find(:undefined_response, & &1) end defp build_response(%{"messages" => %{"resultCode" => "Ok"}} = result, base_response) do @@ -862,10 +883,10 @@ defmodule Gringotts.Gateways.AuthorizeNet do # HELPERS # ############################################################################ - defp set_id(response, id), do: %{response | id: id} - defp set_message(response, message), do: %{response | message: message} + defp set_id(response, id), do: %{response | id: id} + defp set_message(response, message), do: %{response | message: message} defp set_gateway_code(response, code), do: %{response | gateway_code: code} - defp set_reason(response, body), do: %{response | reason: body} + defp set_reason(response, body), do: %{response | reason: body} defp set_avs_result(response, avs_code) do {street, zip_code} = @avs_code_translator[avs_code] @@ -879,6 +900,5 @@ defmodule Gringotts.Gateways.AuthorizeNet do defp set_cavv_result(response, cavv_code) do Map.put(response, :cavv_result, @cavv_code_translator[cavv_code]) end - end end diff --git a/lib/gringotts/gateways/base.ex b/lib/gringotts/gateways/base.ex index be881992..145b4b7a 100644 --- a/lib/gringotts/gateways/base.ex +++ b/lib/gringotts/gateways/base.ex @@ -12,18 +12,18 @@ defmodule Gringotts.Gateways.Base do ``` because this module provides an implementation. """ - + alias Gringotts.Response defmacro __using__(_) do quote location: :keep do @doc false - def purchase(_amount, _card_or_id, _opts) do + def purchase(_amount, _card_or_id, _opts) do not_implemented() end @doc false - def authorize(_amount, _card_or_id, _opts) do + def authorize(_amount, _card_or_id, _opts) do not_implemented() end @@ -57,7 +57,13 @@ defmodule Gringotts.Gateways.Base do {:error, Response.error(code: :not_implemented)} end - defoverridable [purchase: 3, authorize: 3, capture: 3, void: 2, refund: 3, store: 2, unstore: 2] + defoverridable purchase: 3, + authorize: 3, + capture: 3, + void: 2, + refund: 3, + store: 2, + unstore: 2 end end end diff --git a/lib/gringotts/gateways/bogus.ex b/lib/gringotts/gateways/bogus.ex index d6bdaa02..ada7575d 100644 --- a/lib/gringotts/gateways/bogus.ex +++ b/lib/gringotts/gateways/bogus.ex @@ -1,6 +1,6 @@ defmodule Gringotts.Gateways.Bogus do @moduledoc false - + use Gringotts.Gateways.Base alias Gringotts.{ @@ -9,28 +9,20 @@ defmodule Gringotts.Gateways.Bogus do } @some_authorization_id "14a62fff80f24a25f775eeb33624bbb3" - - def authorize(_amount, _card_or_id, _opts), - do: success() - def purchase(_amount, _card_or_id, _opts), - do: success() + def authorize(_amount, _card_or_id, _opts), do: success() + + def purchase(_amount, _card_or_id, _opts), do: success() - def capture(_id, _amount, _opts), - do: success() + def capture(_id, _amount, _opts), do: success() - def void(_id, _opts), - do: success() + def void(_id, _opts), do: success() - def refund(_amount, _id, _opts), - do: success() + def refund(_amount, _id, _opts), do: success() - def store(%CreditCard{} = _card, _opts), - do: success() + def store(%CreditCard{} = _card, _opts), do: success() - def unstore(_customer_id, _opts), - do: success() + def unstore(_customer_id, _opts), do: success() - defp success, - do: {:ok, Response.success(id: @some_authorization_id)} + defp success, do: {:ok, Response.success(id: @some_authorization_id)} end diff --git a/lib/gringotts/gateways/cams.ex b/lib/gringotts/gateways/cams.ex index 12687f6b..2f1d5805 100644 --- a/lib/gringotts/gateways/cams.ex +++ b/lib/gringotts/gateways/cams.ex @@ -408,7 +408,7 @@ defmodule Gringotts.Gateways.Cams do "S" => {nil, nil}, "0" => {nil, nil}, "O" => {nil, nil}, - "" => {nil, nil} + "" => {nil, nil} } # Fetched from CAMS POST API docs. @@ -426,6 +426,7 @@ defmodule Gringotts.Gateways.Cams do {street, zip_code} = @avs_code_translator[decoded_body["avsresponse"]] gateway_code = decoded_body["response_code"] message = decoded_body["responsetext"] + response = %Response{ status_code: 200, id: decoded_body["transactionid"], diff --git a/lib/gringotts/gateways/global_collect.ex b/lib/gringotts/gateways/global_collect.ex index 0c6d143e..624a8950 100644 --- a/lib/gringotts/gateways/global_collect.ex +++ b/lib/gringotts/gateways/global_collect.ex @@ -124,17 +124,15 @@ defmodule Gringotts.Gateways.GlobalCollect do import Poison, only: [decode: 1] - alias Gringotts.{Money, - CreditCard, - Response} - - @brand_map %{ - "visa": "1", - "american_express": "2", - "master": "3", - "discover": "128", - "jcb": "125", - "diners_club": "132" + alias Gringotts.{Money, CreditCard, Response} + + @brand_map %{ + visa: "1", + american_express: "2", + master: "3", + discover: "128", + jcb: "125", + diners_club: "132" } @doc """ @@ -209,7 +207,7 @@ defmodule Gringotts.Gateways.GlobalCollect do ``` """ - @spec capture(String.t(), Money.t, keyword) :: {:ok | :error, Response} + @spec capture(String.t(), Money.t(), keyword) :: {:ok | :error, Response} def capture(payment_id, amount, opts) do params = create_params_for_capture(amount, opts) commit(:post, "payments/#{payment_id}/approve", params, opts) @@ -244,7 +242,7 @@ defmodule Gringotts.Gateways.GlobalCollect do ``` """ - @spec purchase(Money.t, CreditCard.t(), keyword) :: {:ok | :error, Response} + @spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} def purchase(amount, card = %CreditCard{}, opts) do case authorize(amount, card, opts) do {:ok, results} -> @@ -300,7 +298,7 @@ defmodule Gringotts.Gateways.GlobalCollect do iex> {:ok, refund_result} = Gringotts.refund(Gringotts.Gateways.GlobalCollect, auth_result.payment.id, amount) ``` """ - @spec refund(Money.t, String.t(), keyword) :: {:ok | :error, Response} + @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response} def refund(amount, payment_id, opts) do params = create_params_for_refund(amount, opts) commit(:post, "payments/#{payment_id}/refund", params, opts) @@ -329,7 +327,7 @@ defmodule Gringotts.Gateways.GlobalCollect do } end - defp create_params_for_capture(amount, opts) do + defp create_params_for_capture(amount, opts) do %{ order: add_order(amount, opts) } @@ -345,6 +343,7 @@ defmodule Gringotts.Gateways.GlobalCollect do defp add_money(amount, options) do {currency, amount, _} = Money.to_integer(amount) + %{ amount: amount, currencyCode: currency @@ -393,15 +392,16 @@ defmodule Gringotts.Gateways.GlobalCollect do %{ cvv: payment.verification_code, cardNumber: payment.number, - expiryDate: "#{payment.month}"<>"#{payment.year}", + expiryDate: "#{payment.month}" <> "#{payment.year}", cardholderName: CreditCard.full_name(payment) } end defp add_payment(payment, brand_map, opts) do brand = payment.brand + %{ - paymentProductId: Map.fetch!(brand_map, String.to_atom(brand)), + paymentProductId: Map.fetch!(brand_map, String.to_atom(brand)), skipAuthentication: opts[:skipAuthentication], card: add_card(payment) } @@ -422,17 +422,25 @@ defmodule Gringotts.Gateways.GlobalCollect do defp create_headers(path, opts) do time = date - sha_signature = auth_digest(path, opts[:config][:secret_api_key], time, opts) |> Base.encode64 + + sha_signature = + auth_digest(path, opts[:config][:secret_api_key], time, opts) |> Base.encode64() + auth_token = "GCS v1HMAC:#{opts[:config][:api_key_id]}:#{sha_signature}" - headers = [{"Content-Type", "application/json"}, {"Authorization", auth_token}, {"Date", time}] + + headers = [ + {"Content-Type", "application/json"}, + {"Authorization", auth_token}, + {"Date", time} + ] end defp date() do use Timex - datetime = Timex.now |> Timex.local + datetime = Timex.now() |> Timex.local() strftime_str = Timex.format!(datetime, "%a, %d %b %Y %H:%M:%S ", :strftime) time_zone = Timex.timezone(:local, datetime) - time = strftime_str <>"#{time_zone.abbreviation}" + time = strftime_str <> "#{time_zone.abbreviation}" end # Parses GlobalCollect's response and returns a `Gringotts.Response` struct @@ -448,13 +456,17 @@ defmodule Gringotts.Gateways.GlobalCollect do defp respond({:ok, %{status_code: status_code, body: body}}) do {:ok, results} = decode(body) - message = Enum.map(results["errors"],fn (x) -> x["message"] end) + message = Enum.map(results["errors"], fn x -> x["message"] end) detail = List.to_string(message) {:error, Response.error(status_code: status_code, message: detail, raw: results)} end defp respond({:error, %HTTPoison.Error{} = error}) do - {:error, Response.error(code: error.id, reason: :network_fail?, description: "HTTPoison says '#{error.reason}'")} + {:error, + Response.error( + code: error.id, + reason: :network_fail?, + description: "HTTPoison says '#{error.reason}'" + )} end - end diff --git a/lib/gringotts/gateways/paymill.ex b/lib/gringotts/gateways/paymill.ex index 8b00ba3e..5cf177e2 100644 --- a/lib/gringotts/gateways/paymill.ex +++ b/lib/gringotts/gateways/paymill.ex @@ -56,7 +56,7 @@ defmodule Gringotts.Gateways.Paymill do iex> Gringotts.authorize(Gringotts.Gateways.Paymill, amount, card, options) """ - @spec authorize(number, String.t | CreditCard.t, Keyword) :: {:ok | :error, Response} + @spec authorize(number, String.t() | CreditCard.t(), Keyword) :: {:ok | :error, Response} def authorize(amount, card_or_token, options) do Keyword.put(options, :money, amount) action_with_token(:authorize, amount, card_or_token, options) @@ -81,7 +81,7 @@ defmodule Gringotts.Gateways.Paymill do iex> Gringotts.purchase(Gringotts.Gateways.Paymill, amount, card, options) """ - @spec purchase(number, CreditCard.t, Keyword) :: {:ok | :error, Response} + @spec purchase(number, CreditCard.t(), Keyword) :: {:ok | :error, Response} def purchase(amount, card, options) do Keyword.put(options, :money, amount) action_with_token(:purchase, amount, card, options) @@ -99,7 +99,7 @@ defmodule Gringotts.Gateways.Paymill do iex> Gringotts.capture(Gringotts.Gateways.Paymill, token, amount, options) """ - @spec capture(String.t, number, Keyword) :: {:ok | :error, Response} + @spec capture(String.t(), number, Keyword) :: {:ok | :error, Response} def capture(authorization, amount, options) do post = add_amount([], amount, options) ++ [{"preauthorization", authorization}] @@ -116,13 +116,13 @@ defmodule Gringotts.Gateways.Paymill do iex> Gringotts.void(Gringotts.Gateways.Paymill, token, options) """ - @spec void(String.t, Keyword) :: {:ok | :error, Response} + @spec void(String.t(), Keyword) :: {:ok | :error, Response} def void(authorization, options) do commit(:delete, "preauthorizations/#{authorization}", [], options) end @doc false - @spec authorize_with_token(number, String.t, Keyword) :: term + @spec authorize_with_token(number, String.t(), Keyword) :: term def authorize_with_token(money, card_token, options) do post = add_amount([], money, options) ++ [{"token", card_token}] @@ -130,51 +130,53 @@ defmodule Gringotts.Gateways.Paymill do end @doc false - @spec purchase_with_token(number, String.t, Keyword) :: term + @spec purchase_with_token(number, String.t(), Keyword) :: term def purchase_with_token(money, card_token, options) do post = add_amount([], money, options) ++ [{"token", card_token}] commit(:post, "transactions", post, options) end - @spec save_card(CreditCard.t, Keyword) :: Response + @spec save_card(CreditCard.t(), Keyword) :: Response defp save_card(card, options) do - {:ok, %HTTPoison.Response{body: response}} = HTTPoison.get( + {:ok, %HTTPoison.Response{body: response}} = + HTTPoison.get( get_save_card_url(), get_headers(options), - params: get_save_card_params(card, options)) + params: get_save_card_params(card, options) + ) - parse_card_response(response) + parse_card_response(response) end - @spec save(CreditCard.t, Keyword) :: Response + @spec save(CreditCard.t(), Keyword) :: Response defp save(card, options) do save_card(card, options) end defp action_with_token(action, amount, "tok_" <> id = card_token, options) do - apply(__MODULE__, String.to_atom("#{action}_with_token"), [amount, card_token , options]) + apply(__MODULE__, String.to_atom("#{action}_with_token"), [amount, card_token, options]) end defp action_with_token(action, amount, %CreditCard{} = card, options) do {:ok, response} = save_card(card, options) card_token = get_token(response) - apply(__MODULE__, String.to_atom("#{action}_with_token"), [amount, card_token , options]) + apply(__MODULE__, String.to_atom("#{action}_with_token"), [amount, card_token, options]) end defp get_save_card_params(card, options) do - [ - {"transaction.mode" , "CONNECTOR_TEST"}, - {"channel.id" , get_config(:public_key, options)}, - {"jsonPFunction" , "jsonPFunction"}, - {"account.number" , card.number}, - {"account.expiry.month" , card.month}, - {"account.expiry.year" , card.year}, - {"account.verification" , card.verification_code}, - {"account.holder" , "#{card.first_name} #{card.last_name}"}, - {"presentation.amount3D" , get_amount(options)}, - {"presentation.currency3D" , get_currency(options)} + [ + {"transaction.mode", "CONNECTOR_TEST"}, + {"channel.id", get_config(:public_key, options)}, + {"jsonPFunction", "jsonPFunction"}, + {"account.number", card.number}, + {"account.expiry.month", card.month}, + {"account.expiry.year", card.year}, + {"account.verification", card.verification_code}, + {"account.holder", CreditCard.full_name(card)}, + {"presentation.amount3D", get_amount(options)}, + {"presentation.currency3D", get_currency(options)} ] end @@ -196,7 +198,7 @@ defmodule Gringotts.Gateways.Paymill do response |> String.replace(~r/jsonPFunction\(/, "") |> String.replace(~r/\)/, "") - |> Poison.decode + |> Poison.decode() end defp get_currency(options), do: options[:currency] || @default_currency @@ -210,7 +212,7 @@ defmodule Gringotts.Gateways.Paymill do defp commit(method, action, parameters \\ nil, options) do method |> HTTPoison.request(@live_url <> action, {:form, parameters}, get_headers(options), []) - |> ResponseParser.parse + |> ResponseParser.parse() end defp get_config(key, options) do @@ -222,118 +224,118 @@ defmodule Gringotts.Gateways.Paymill do alias Gringotts.Response @response_code %{ - 10_001 => "Undefined response", - 10_002 => "Waiting for something", - 11_000 => "Retry request at a later time", - - 20_000 => "Operation successful", - 20_100 => "Funds held by acquirer", - 20_101 => "Funds held by acquirer because merchant is new", - 20_200 => "Transaction reversed", - 20_201 => "Reversed due to chargeback", - 20_202 => "Reversed due to money-back guarantee", - 20_203 => "Reversed due to complaint by buyer", - 20_204 => "Payment has been refunded", - 20_300 => "Reversal has been canceled", - 22_000 => "Initiation of transaction successful", - - 30_000 => "Transaction still in progress", - 30_100 => "Transaction has been accepted", - 31_000 => "Transaction pending", - 31_100 => "Pending due to address", - 31_101 => "Pending due to uncleared eCheck", - 31_102 => "Pending due to risk review", - 31_103 => "Pending due regulatory review", - 31_104 => "Pending due to unregistered/unconfirmed receiver", - 31_200 => "Pending due to unverified account", - 31_201 => "Pending due to non-captured funds", - 31_202 => "Pending due to international account (accept manually)", - 31_203 => "Pending due to currency conflict (accept manually)", - 31_204 => "Pending due to fraud filters (accept manually)", - - 40_000 => "Problem with transaction data", - 40_001 => "Problem with payment data", - 40_002 => "Invalid checksum", - 40_100 => "Problem with credit card data", - 40_101 => "Problem with CVV", - 40_102 => "Card expired or not yet valid", - 40_103 => "Card limit exceeded", - 40_104 => "Card is not valid", - 40_105 => "Expiry date not valid", - 40_106 => "Credit card brand required", - 40_200 => "Problem with bank account data", - 40_201 => "Bank account data combination mismatch", - 40_202 => "User authentication failed", - 40_300 => "Problem with 3-D Secure data", - 40_301 => "Currency/amount mismatch", - 40_400 => "Problem with input data", - 40_401 => "Amount too low or zero", - 40_402 => "Usage field too long", - 40_403 => "Currency not allowed", - 40_410 => "Problem with shopping cart data", - 40_420 => "Problem with address data", - 40_500 => "Permission error with acquirer API", - 40_510 => "Rate limit reached for acquirer API", - 42_000 => "Initiation of transaction failed", - 42_410 => "Initiation of transaction expired", - - 50_000 => "Problem with back end", - 50_001 => "Country blacklisted", - 50_002 => "IP address blacklisted", - 50_004 => "Live mode not allowed", - 50_005 => "Insufficient permissions (API key)", - 50_100 => "Technical error with credit card", - 50_101 => "Error limit exceeded", - 50_102 => "Card declined", - 50_103 => "Manipulation or stolen card", - 50_104 => "Card restricted", - 50_105 => "Invalid configuration data", - 50_200 => "Technical error with bank account", - 50_201 => "Account blacklisted", - 50_300 => "Technical error with 3-D Secure", - 50_400 => "Declined because of risk issues", - 50_401 => "Checksum was wrong", - 50_402 => "Bank account number was invalid (formal check)", - 50_403 => "Technical error with risk check", - 50_404 => "Unknown error with risk check", - 50_405 => "Unknown bank code", - 50_406 => "Open chargeback", - 50_407 => "Historical chargeback", - 50_408 => "Institution / public bank account (NCA)", - 50_409 => "KUNO/Fraud", - 50_410 => "Personal Account Protection (PAP)", - 50_420 => "Rejected due to acquirer fraud settings", - 50_430 => "Rejected due to acquirer risk settings", - 50_440 => "Failed due to restrictions with acquirer account", - 50_450 => "Failed due to restrictions with user account", - 50_500 => "General timeout", - 50_501 => "Timeout on side of the acquirer", - 50_502 => "Risk management transaction timeout", - 50_600 => "Duplicate operation", - 50_700 => "Cancelled by user", - 50_710 => "Failed due to funding source", - 50_711 => "Payment method not usable, use other payment method", - 50_712 => "Limit of funding source was exceeded", - 50_713 => "Means of payment not reusable (canceled by user)", - 50_714 => "Means of payment not reusable (expired)", - 50_720 => "Rejected by acquirer", - 50_730 => "Transaction denied by merchant", - 50_800 => "Preauthorisation failed", - 50_810 => "Authorisation has been voided", - 50_820 => "Authorisation period expired" - } + 10_001 => "Undefined response", + 10_002 => "Waiting for something", + 11_000 => "Retry request at a later time", + 20_000 => "Operation successful", + 20_100 => "Funds held by acquirer", + 20_101 => "Funds held by acquirer because merchant is new", + 20_200 => "Transaction reversed", + 20_201 => "Reversed due to chargeback", + 20_202 => "Reversed due to money-back guarantee", + 20_203 => "Reversed due to complaint by buyer", + 20_204 => "Payment has been refunded", + 20_300 => "Reversal has been canceled", + 22_000 => "Initiation of transaction successful", + 30_000 => "Transaction still in progress", + 30_100 => "Transaction has been accepted", + 31_000 => "Transaction pending", + 31_100 => "Pending due to address", + 31_101 => "Pending due to uncleared eCheck", + 31_102 => "Pending due to risk review", + 31_103 => "Pending due regulatory review", + 31_104 => "Pending due to unregistered/unconfirmed receiver", + 31_200 => "Pending due to unverified account", + 31_201 => "Pending due to non-captured funds", + 31_202 => "Pending due to international account (accept manually)", + 31_203 => "Pending due to currency conflict (accept manually)", + 31_204 => "Pending due to fraud filters (accept manually)", + 40_000 => "Problem with transaction data", + 40_001 => "Problem with payment data", + 40_002 => "Invalid checksum", + 40_100 => "Problem with credit card data", + 40_101 => "Problem with CVV", + 40_102 => "Card expired or not yet valid", + 40_103 => "Card limit exceeded", + 40_104 => "Card is not valid", + 40_105 => "Expiry date not valid", + 40_106 => "Credit card brand required", + 40_200 => "Problem with bank account data", + 40_201 => "Bank account data combination mismatch", + 40_202 => "User authentication failed", + 40_300 => "Problem with 3-D Secure data", + 40_301 => "Currency/amount mismatch", + 40_400 => "Problem with input data", + 40_401 => "Amount too low or zero", + 40_402 => "Usage field too long", + 40_403 => "Currency not allowed", + 40_410 => "Problem with shopping cart data", + 40_420 => "Problem with address data", + 40_500 => "Permission error with acquirer API", + 40_510 => "Rate limit reached for acquirer API", + 42_000 => "Initiation of transaction failed", + 42_410 => "Initiation of transaction expired", + 50_000 => "Problem with back end", + 50_001 => "Country blacklisted", + 50_002 => "IP address blacklisted", + 50_004 => "Live mode not allowed", + 50_005 => "Insufficient permissions (API key)", + 50_100 => "Technical error with credit card", + 50_101 => "Error limit exceeded", + 50_102 => "Card declined", + 50_103 => "Manipulation or stolen card", + 50_104 => "Card restricted", + 50_105 => "Invalid configuration data", + 50_200 => "Technical error with bank account", + 50_201 => "Account blacklisted", + 50_300 => "Technical error with 3-D Secure", + 50_400 => "Declined because of risk issues", + 50_401 => "Checksum was wrong", + 50_402 => "Bank account number was invalid (formal check)", + 50_403 => "Technical error with risk check", + 50_404 => "Unknown error with risk check", + 50_405 => "Unknown bank code", + 50_406 => "Open chargeback", + 50_407 => "Historical chargeback", + 50_408 => "Institution / public bank account (NCA)", + 50_409 => "KUNO/Fraud", + 50_410 => "Personal Account Protection (PAP)", + 50_420 => "Rejected due to acquirer fraud settings", + 50_430 => "Rejected due to acquirer risk settings", + 50_440 => "Failed due to restrictions with acquirer account", + 50_450 => "Failed due to restrictions with user account", + 50_500 => "General timeout", + 50_501 => "Timeout on side of the acquirer", + 50_502 => "Risk management transaction timeout", + 50_600 => "Duplicate operation", + 50_700 => "Cancelled by user", + 50_710 => "Failed due to funding source", + 50_711 => "Payment method not usable, use other payment method", + 50_712 => "Limit of funding source was exceeded", + 50_713 => "Means of payment not reusable (canceled by user)", + 50_714 => "Means of payment not reusable (expired)", + 50_720 => "Rejected by acquirer", + 50_730 => "Transaction denied by merchant", + 50_800 => "Preauthorisation failed", + 50_810 => "Authorisation has been voided", + 50_820 => "Authorisation period expired" + } def parse({:ok, %HTTPoison.Response{body: body, status_code: 200}}) do body = Poison.decode!(body) parse_body(body) end + def parse({:ok, %HTTPoison.Response{body: body, status_code: 400}}) do body = Poison.decode!(body) + [] |> set_params(body) end + def parse({:ok, %HTTPoison.Response{body: body, status_code: 404}}) do body = Poison.decode!(body) + [] |> set_success(body) |> set_params(body) @@ -343,6 +345,7 @@ defmodule Gringotts.Gateways.Paymill do defp set_success(opts, %{"error" => error}) do opts ++ [message: error, success: false] end + defp set_success(opts, %{"transaction" => %{"response_code" => 20_000}}) do opts ++ [success: true] end @@ -363,31 +366,33 @@ defmodule Gringotts.Gateways.Paymill do end end - #Status code + # Status code defp parse_status_code(opts, %{"status" => "failed"} = body) do response_code = get_in(body, ["transaction", "response_code"]) response_msg = Map.get(@response_code, response_code, -1) opts ++ [message: response_msg] end + defp parse_status_code(opts, %{"transaction" => transaction}) do response_code = Map.get(transaction, "response_code", -1) response_msg = Map.get(@response_code, response_code, -1) opts ++ [status_code: response_code, message: response_msg] end + defp parse_status_code(opts, %{"response_code" => code}) do response_msg = Map.get(@response_code, code, -1) opts ++ [status_code: code, message: response_msg] end - #Authorization + # Authorization defp parse_authorization(opts, %{"status" => "failed"}) do opts ++ [success: false] end + defp parse_authorization(opts, %{"id" => id} = auth) do opts ++ [authorization: id] end defp set_params(opts, body), do: opts ++ [params: body] end - end diff --git a/lib/gringotts/gateways/wire_card.ex b/lib/gringotts/gateways/wire_card.ex index 5aafeb37..939a0fef 100644 --- a/lib/gringotts/gateways/wire_card.ex +++ b/lib/gringotts/gateways/wire_card.ex @@ -1,5 +1,6 @@ # call => Gringotts.Gateways.WireCard.authorize(100, creditcard, options) import XmlBuilder + defmodule Gringotts.Gateways.WireCard do @moduledoc """ WireCard System Plugins @@ -7,7 +8,7 @@ defmodule Gringotts.Gateways.WireCard do @test_url "https://c3-test.wirecard.com/secure/ssl-gateway" @live_url "https://c3.wirecard.com/secure/ssl-gateway" @homepage_url "http://www.wirecard.com" - + @doc """ Wirecard only allows phone numbers with a format like this: +xxx(yyy)zzz-zzzz-ppp, where: xxx = Country code @@ -18,8 +19,8 @@ defmodule Gringotts.Gateways.WireCard do number 5551234 within area code 202 (country code 1). """ @valid_phone_format ~r/\+\d{1,3}(\(?\d{3}\)?)?\d{3}-\d{4}-\d{3}/ - @default_currency "EUR" - @default_amount 100 + @default_currency "EUR" + @default_amount 100 use Gringotts.Gateways.Base use Gringotts.Adapter, required_config: [:login, :password, :signature] @@ -38,10 +39,10 @@ defmodule Gringotts.Gateways.WireCard do then then the :recurring option will be forced to "Repeated" =========================================================== TODO: Mandatorily check for :login,:password, :signature in options - Note: payment_menthod for now is only credit_card and + Note: payment_menthod for now is only credit_card and TODO: change it so it can also have GuWID ================================================ - E.g: => + E.g: => creditcard = %CreditCard{ number: "4200000000000000", month: 12, @@ -65,19 +66,19 @@ defmodule Gringotts.Gateways.WireCard do } options = [ config: %{ - login: "00000031629CA9FA", + login: "00000031629CA9FA", password: "TestXAPTER", signature: "00000031629CAFD5", - }, + }, order_id: 1, billing_address: address, description: 'Wirecard remote test purchase', email: "soleone@example.com", ip: "127.0.0.1", test: true - ] + ] """ - @spec authorize(Integer | Float, CreditCard.t | String.t, Keyword) :: {:ok, Map} + @spec authorize(Integer | Float, CreditCard.t() | String.t(), Keyword) :: {:ok, Map} def authorize(money, payment_method, options \\ []) def authorize(money, %CreditCard{} = creditcard, options) do @@ -92,9 +93,9 @@ defmodule Gringotts.Gateways.WireCard do @doc """ Capture - the first paramter here should be a GuWid/authorization. - Authorization is obtained by authorizing the creditcard. + Authorization is obtained by authorizing the creditcard. """ - @spec capture(String.t, Float, Keyword) :: {:ok, Map} + @spec capture(String.t(), Float, Keyword) :: {:ok, Map} def capture(authorization, money, options \\ []) when is_binary(authorization) do options = Keyword.put(options, :preauthorization, authorization) commit(:post, :capture, money, options) @@ -106,7 +107,7 @@ defmodule Gringotts.Gateways.WireCard do transaction. If a GuWID is given, rather than a CreditCard, then then the :recurring option will be forced to "Repeated" """ - @spec purchase(Float | Integer, CreditCard| String.t, Keyword) :: {:ok, Map} + @spec purchase(Float | Integer, CreditCard | String.t(), Keyword) :: {:ok, Map} def purchase(money, payment_method, options \\ []) def purchase(money, %CreditCard{} = creditcard, options) do @@ -120,33 +121,33 @@ defmodule Gringotts.Gateways.WireCard do end @doc """ - Void - A credit card purchase that a seller cancels after it has - been authorized but before it has been settled. - A void transaction does not appear on the customer's + Void - A credit card purchase that a seller cancels after it has + been authorized but before it has been settled. + A void transaction does not appear on the customer's credit card statement, though it might appear in a list - of pending transactions when the customer checks their + of pending transactions when the customer checks their account online. ==== Parameters ====== identification - The authorization string returned from the initial authorization or purchase. """ - @spec void(String.t, Keyword) :: {:ok, Map} + @spec void(String.t(), Keyword) :: {:ok, Map} def void(identification, options \\ []) when is_binary(identification) do options = Keyword.put(options, :preauthorization, identification) commit(:post, :reversal, nil, options) end - + @doc """ Performs a credit. - - This transaction indicates that money - should flow from the merchant to the customer. + + This transaction indicates that money + should flow from the merchant to the customer. ==== Parameters ==== - money -- The amount to be credited to the customer + money -- The amount to be credited to the customer as an Integer value in cents. identification -- GuWID """ - @spec refund(Float, String.t, Keyword) :: {:ok, Map} + @spec refund(Float, String.t(), Keyword) :: {:ok, Map} def refund(money, identification, options \\ []) when is_binary(identification) do options = Keyword.put(options, :preauthorization, identification) commit(:post, :bookback, money, options) @@ -161,54 +162,61 @@ defmodule Gringotts.Gateways.WireCard do "RECURRING_TRANSACTION/Type" set to "Initial". Subsequent transactions can then use the GuWID in place of a credit card by setting "RECURRING_TRANSACTION/Type" to "Repeated". - + This implementation of card store utilizes a Wirecard "Authorization Check" (a Preauthorization that is automatically reversed). It defaults to a check amount of "100" (i.e. $1.00) but this can be overriden (see below). - + IMPORTANT: In order to reuse the stored reference, the +authorization+ from the response should be saved by your application code. - + ==== Options specific to +store+ - + * :amount -- The amount, in cents, that should be "validated" by the Authorization Check. This amount will be reserved and then reversed. Default is 100. - + Note: This is not the only way to achieve a card store operation at Wirecard. Any +purchase+ or +authorize+ can be sent with +options[:recurring] = 'Initial'+ to make the returned authorization/GuWID usable in later transactions with +options[:recurring] = 'Repeated'+. """ - @spec store(CreditCard.t, Keyword) :: {:ok, Map} + @spec store(CreditCard.t(), Keyword) :: {:ok, Map} def store(%CreditCard{} = creditcard, options \\ []) do - options = options - |> Keyword.put(:credit_card, creditcard) - |> Keyword.put(:recurring, "Initial") + options = + options + |> Keyword.put(:credit_card, creditcard) + |> Keyword.put(:recurring, "Initial") + money = options[:amount] || @default_amount # Amex does not support authorization_check case creditcard.brand do "american_express" -> commit(:post, :preauthorization, money, options) - _ -> commit(:post, :authorization_check, money, options) + _ -> commit(:post, :authorization_check, money, options) end end - - # =================== Private Methods =================== - + + # =================== Private Methods =================== + # Contact WireCard, make the XML request, and parse the # reply into a Response object. defp commit(method, action, money, options) do # TODO: validate and setup address hash as per AM request = build_request(action, money, options) - headers = %{"Content-Type" => "text/xml", - "Authorization" => encoded_credentials( - options[:config][:login], options[:config][:password] - ) - } - method |> HTTPoison.request(base_url(options) , request, headers) |> respond + + headers = %{ + "Content-Type" => "text/xml", + "Authorization" => + encoded_credentials( + options[:config][:login], + options[:config][:password] + ) + } + + method |> HTTPoison.request(base_url(options), request, headers) |> respond end defp respond({:ok, %{status_code: 200, body: body}}) do @@ -217,13 +225,13 @@ defmodule Gringotts.Gateways.WireCard do end defp respond({:ok, %{body: body, status_code: status_code}}) do - {:error, "Some Error Occurred: \n #{ inspect body }"} + {:error, "Some Error Occurred: \n #{inspect(body)}"} end # Read the XML message from the gateway and check if it was successful, # and also extract required return values from the response # TODO: parse XML Response - defp parse(data) do + defp parse(data) do XmlToMap.naive_map(data) end @@ -231,15 +239,18 @@ defmodule Gringotts.Gateways.WireCard do defp build_request(action, money, options) do options = Keyword.put(options, :action, action) - request = doc(element(:WIRECARD_BXML, [ - element(:W_REQUEST, [ - element(:W_JOB, [ - element(:JobID, ""), - element(:BusinessCaseSignature, options[:config][:signature]), - add_transaction_data(action, money, options) + request = + doc( + element(:WIRECARD_BXML, [ + element(:W_REQUEST, [ + element(:W_JOB, [ + element(:JobID, ""), + element(:BusinessCaseSignature, options[:config][:signature]), + add_transaction_data(action, money, options) + ]) ]) ]) - ])) + ) request end @@ -250,11 +261,14 @@ defmodule Gringotts.Gateways.WireCard do defp add_transaction_data(action, money, options) do element("FNC_CC_#{atom_to_upcase_string(options[:action])}", [ element(:FunctionID, "dummy_description"), - element(:CC_TRANSACTION, [ - element(:TransactionID, options[:order_id]), - element(:CommerceType, (if options[:commerce_type], do: options[:commerce_type])) - ] ++ add_action_data(action, money, options) ++ add_customer_data(options) - )]) + element( + :CC_TRANSACTION, + [ + element(:TransactionID, options[:order_id]), + element(:CommerceType, if(options[:commerce_type], do: options[:commerce_type])) + ] ++ add_action_data(action, money, options) ++ add_customer_data(options) + ) + ]) end # Includes the IP address of the customer to the transaction-xml @@ -269,9 +283,14 @@ defmodule Gringotts.Gateways.WireCard do def add_action_data(action, money, options) do case options[:action] do # returns array of elements - action when(action in [:preauthorization, :purchase, :authorization_check]) -> create_elems_for_preauth_or_purchase_or_auth_check(money, options) - action when(action in [:capture, :bookback]) -> create_elems_for_capture_or_bookback(money, options) - action when(action == :reversal) -> add_guwid(options[:preauthorization]) + action when action in [:preauthorization, :purchase, :authorization_check] -> + create_elems_for_preauth_or_purchase_or_auth_check(money, options) + + action when action in [:capture, :bookback] -> + create_elems_for_capture_or_bookback(money, options) + + action when action == :reversal -> + add_guwid(options[:preauthorization]) end end @@ -280,25 +299,29 @@ defmodule Gringotts.Gateways.WireCard do add_guwid(options[:preauthorization]) ++ [add_amount(money, options)] end - # Creates xml request elements if action is preauth, purchase ir auth_check + # Creates xml request elements if action is preauth, purchase ir auth_check # TODO: handle nil values if array not generated defp create_elems_for_preauth_or_purchase_or_auth_check(money, options) do # TODO: setup_recurring_flag - add_invoice(money, options) ++ element_for_credit_card_or_guwid(options) ++ add_address(options[:billing_address]) + add_invoice(money, options) ++ + element_for_credit_card_or_guwid(options) ++ add_address(options[:billing_address]) end - + defp add_address(address) do if address do [ element(:CORPTRUSTCENTER_DATA, [ element(:ADDRESS, [ element(:Address1, address[:address1]), - element(:Address2, (if address[:address2], do: address[:address2])), + element(:Address2, if(address[:address2], do: address[:address2])), element(:City, address[:city]), - element(:Zip, address[:zip]), + element(:Zip, address[:zip]), add_state(address), element(:Country, address[:country]), - element(:Phone, (if regex_match(@valid_phone_format, address[:phone]), do: address[:phone])), + element( + :Phone, + if(regex_match(@valid_phone_format, address[:phone]), do: address[:phone]) + ), element(:Email, address[:email]) ]) ]) @@ -307,9 +330,9 @@ defmodule Gringotts.Gateways.WireCard do end defp add_state(address) do - if (regex_match(~r/[A-Za-z]{2}/, address[:state]) && regex_match(~r/^(us|ca)$/i, address[:country]) - ) do - element(:State, (String.upcase(address[:state]))) + if regex_match(~r/[A-Za-z]{2}/, address[:state]) && + regex_match(~r/^(us|ca)$/i, address[:country]) do + element(:State, String.upcase(address[:state])) end end @@ -320,7 +343,7 @@ defmodule Gringotts.Gateways.WireCard do add_guwid(options[:preauthorization]) end end - + # Includes Guwid data to transaction-xml defp add_guwid(preauth) do [element(:GuWID, preauth)] @@ -329,13 +352,15 @@ defmodule Gringotts.Gateways.WireCard do # Includes the credit-card data to the transaction-xml # TODO: Format Credit Card month, ref AM defp add_creditcard(creditcard) do - [element(:CREDIT_CARD_DATA, [ - element(:CreditCardNumber, creditcard.number), - element(:CVC2, creditcard.verification_code), - element(:ExpirationYear, creditcard.year), - element(:ExpirationMonth, creditcard.month), - element(:CardHolderName, join_string([creditcard.first_name, creditcard.last_name], " ")) - ])] + [ + element(:CREDIT_CARD_DATA, [ + element(:CreditCardNumber, creditcard.number), + element(:CVC2, creditcard.verification_code), + element(:ExpirationYear, creditcard.year), + element(:ExpirationMonth, creditcard.month), + element(:CardHolderName, join_string([creditcard.first_name, creditcard.last_name], " ")) + ]) + ] end # Includes the payment (amount, currency, country) to the transaction-xml @@ -345,11 +370,11 @@ defmodule Gringotts.Gateways.WireCard do element(:Currency, currency(options)), element(:CountryCode, options[:billing_address][:country]), element(:RECURRING_TRANSACTION, [ - element(:Type, (options[:recurring] || "Single")) + element(:Type, options[:recurring] || "Single") ]) ] end - + # Include the amount in the transaction-xml # TODO: check for localized currency or currency # localized_amount(money, options[:currency] || currency(money)) @@ -357,24 +382,24 @@ defmodule Gringotts.Gateways.WireCard do defp atom_to_upcase_string(atom) do atom - |> to_string - |> String.upcase + |> to_string + |> String.upcase() end # Encode login and password in Base64 to supply as HTTP header # (for http basic authentication) defp encoded_credentials(login, password) do [login, password] - |> join_string(":") - |> Base.encode64 - |> (&("Basic "<> &1)).() + |> join_string(":") + |> Base.encode64() + |> (&("Basic " <> &1)).() end defp join_string(list_of_words, joiner), do: Enum.join(list_of_words, joiner) defp regex_match(regex, string), do: Regex.match?(regex, string) - defp base_url(opts), do: if opts[:test], do: @test_url, else: @live_url + defp base_url(opts), do: if(opts[:test], do: @test_url, else: @live_url) defp currency(opts), do: opts[:currency] || @default_currency end diff --git a/lib/gringotts/response.ex b/lib/gringotts/response.ex index ac369f89..c64ec0a2 100644 --- a/lib/gringotts/response.ex +++ b/lib/gringotts/response.ex @@ -8,8 +8,17 @@ defmodule Gringotts.Response do """ defstruct [ - :success, :id, :token, :status_code, :gateway_code, :reason, :message, - :avs_result, :cvc_result, :raw, :fraud_review + :success, + :id, + :token, + :status_code, + :gateway_code, + :reason, + :message, + :avs_result, + :cvc_result, + :raw, + :fraud_review ] @typedoc """ @@ -54,19 +63,19 @@ defmodule Gringotts.Response do [cvc]: https://en.wikipedia.org/wiki/Card_security_code """ - @type t:: %__MODULE__{ - success: boolean, - id: String.t, - token: String.t, - status_code: non_neg_integer, - gateway_code: String.t, - reason: String.t, - message: String.t, - avs_result: %{street: String.t, zip_code: String.t}, - cvc_result: String.t, - raw: String.t, - fraud_review: term - } + @type t :: %__MODULE__{ + success: boolean, + id: String.t(), + token: String.t(), + status_code: non_neg_integer, + gateway_code: String.t(), + reason: String.t(), + message: String.t(), + avs_result: %{street: String.t(), zip_code: String.t()}, + cvc_result: String.t(), + raw: String.t(), + fraud_review: term + } def success(opts \\ []) do new(true, opts) diff --git a/mix.exs b/mix.exs index ef54cc7f..244a6405 100644 --- a/mix.exs +++ b/mix.exs @@ -17,14 +17,14 @@ defmodule Gringotts.Mixfile do tool: ExCoveralls ], preferred_cli_env: [ - "coveralls": :test, + coveralls: :test, "coveralls.detail": :test, - "coveralls.post": :test, - "coveralls.html": :test, - "coveralls.travis": :test + "coveralls.json": :test, + "coveralls.html": :test ], deps: deps(), - docs: docs()] + docs: docs() + ] end # Configuration for the OTP application @@ -32,7 +32,7 @@ defmodule Gringotts.Mixfile do # Type `mix help compile.app` for more information def application do [ - applications: [:httpoison, :hackney, :elixir_xml_to_map, :timex], + applications: [:httpoison, :hackney, :elixir_xml_to_map, :timex] ] end @@ -89,8 +89,8 @@ defmodule Gringotts.Mixfile do end defp groups_for_modules do - [ - "Gateways": ~r/^Gringotts.Gateways.?/, - ] + [ + Gateways: ~r/^Gringotts.Gateways.?/ + ] end end diff --git a/test/gateways/authorize_net_test.exs b/test/gateways/authorize_net_test.exs index 2ce52278..cec0b0cd 100644 --- a/test/gateways/authorize_net_test.exs +++ b/test/gateways/authorize_net_test.exs @@ -103,8 +103,8 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do customer_type: "individual" ] @opts_customer_profile_args [ - config: @auth, - customer_profile_id: "1814012002" + config: @auth, + customer_profile_id: "1814012002" ] @refund_id "60036752756" @@ -124,7 +124,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do post: fn _url, _body, _headers -> MockResponse.successful_purchase_response() end do - assert {:ok, response} = ANet.purchase(@amount, @card, @opts) + assert {:ok, _response} = ANet.purchase(@amount, @card, @opts) end end @@ -144,7 +144,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do post: fn _url, _body, _headers -> MockResponse.successful_authorize_response() end do - assert {:ok, response} = ANet.authorize(@amount, @card, @opts) + assert {:ok, _response} = ANet.authorize(@amount, @card, @opts) end end @@ -164,13 +164,12 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do post: fn _url, _body, _headers -> MockResponse.successful_capture_response() end do - assert {:ok, response} = ANet.capture(@capture_id, @amount, @opts) + assert {:ok, _response} = ANet.capture(@capture_id, @amount, @opts) end end test "with bad transaction id" do - with_mock HTTPoison, - post: fn _url, _body, _headers -> MockResponse.bad_id_capture() end do + with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.bad_id_capture() end do assert {:error, response} = ANet.capture(@capture_invalid_id, @amount, @opts) end end @@ -182,13 +181,12 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do post: fn _url, _body, _headers -> MockResponse.successful_refund_response() end do - assert {:ok, response} = ANet.refund(@amount, @refund_id, @opts_refund) + assert {:ok, _response} = ANet.refund(@amount, @refund_id, @opts_refund) end end test "bad payment params" do - with_mock HTTPoison, - post: fn _url, _body, _headers -> MockResponse.bad_card_refund() end do + with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.bad_card_refund() end do assert {:error, response} = ANet.refund(@amount, @refund_id, @opts_refund_bad_payment) end end @@ -203,9 +201,8 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do describe "void" do test "successful response with right params" do - with_mock HTTPoison, - post: fn _url, _body, _headers -> MockResponse.successful_void() end do - assert {:ok, response} = ANet.void(@void_id, @opts) + with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_void() end do + assert {:ok, _response} = ANet.void(@void_id, @opts) end end @@ -221,14 +218,14 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do test "successful response with right params" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_store_response() end do - assert {:ok, response} = ANet.store(@card, @opts_store) + assert {:ok, _response} = ANet.store(@card, @opts_store) end end test "successful response without validation and customer type" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_store_response() end do - assert {:ok, response} = ANet.store(@card, @opts_store_without_validation) + assert {:ok, _response} = ANet.store(@card, @opts_store_without_validation) end end @@ -239,7 +236,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do end do assert {:error, response} = ANet.store(@card, @opts_store_no_profile) - "Error" + "Error" end end @@ -248,16 +245,16 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do post: fn _url, _body, _headers -> MockResponse.customer_payment_profile_success_response() end do - assert {:ok, response} = ANet.store(@card, @opts_customer_profile) + assert {:ok, _response} = ANet.store(@card, @opts_customer_profile) - "Ok" + "Ok" end end test "successful response without valiadtion mode and customer type" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_store_response() end do - assert {:ok, response} = ANet.store(@card, @opts_customer_profile_args) + assert {:ok, _response} = ANet.store(@card, @opts_customer_profile_args) end end end @@ -268,7 +265,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do post: fn _url, _body, _headers -> MockResponse.successful_unstore_response() end do - assert {:ok, response} = ANet.unstore(@unstore_id, @opts) + assert {:ok, _response} = ANet.unstore(@unstore_id, @opts) end end end diff --git a/test/gateways/bogus_test.exs b/test/gateways/bogus_test.exs index 041c1469..9235bcf8 100644 --- a/test/gateways/bogus_test.exs +++ b/test/gateways/bogus_test.exs @@ -6,51 +6,44 @@ defmodule Gringotts.Gateways.BogusTest do @some_id "some_arbitrary_id" @amount Money.new(5, :USD) - + test "authorize" do - {:ok, %Response{id: id, success: success}} = - Gateway.authorize(@amount, :card, []) + {:ok, %Response{id: id, success: success}} = Gateway.authorize(@amount, :card, []) assert success assert id != nil end test "purchase" do - {:ok, %Response{id: id, success: success}} = - Gateway.purchase(@amount, :card, []) + {:ok, %Response{id: id, success: success}} = Gateway.purchase(@amount, :card, []) assert success assert id != nil end test "capture" do - {:ok, %Response{id: id, success: success}} = - Gateway.capture(@some_id, @amount, []) + {:ok, %Response{id: id, success: success}} = Gateway.capture(@some_id, @amount, []) assert success assert id != nil end test "void" do - {:ok, %Response{id: id, success: success}} = - Gateway.void(@some_id, []) + {:ok, %Response{id: id, success: success}} = Gateway.void(@some_id, []) assert success assert id != nil end test "store" do - {:ok, %Response{success: success}} = - Gateway.store(%Gringotts.CreditCard{}, []) + {:ok, %Response{success: success}} = Gateway.store(%Gringotts.CreditCard{}, []) assert success end test "unstore with customer" do - {:ok, %Response{success: success}} = - Gateway.unstore(@some_id, []) + {:ok, %Response{success: success}} = Gateway.unstore(@some_id, []) assert success end - end diff --git a/test/gateways/cams_test.exs b/test/gateways/cams_test.exs index edf0e5f4..a6117a78 100644 --- a/test/gateways/cams_test.exs +++ b/test/gateways/cams_test.exs @@ -68,8 +68,7 @@ defmodule Gringotts.Gateways.CamsTest do test "with bad card" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.failed_purchase_with_bad_credit_card() end do - {:error, %Response{reason: reason}} = - Gateway.purchase(@money, @bad_card, @options) + {:error, %Response{reason: reason}} = Gateway.purchase(@money, @bad_card, @options) assert String.contains?(reason, "Invalid Credit Card Number") end @@ -95,8 +94,7 @@ defmodule Gringotts.Gateways.CamsTest do test "with bad card" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.failed_authorized_with_bad_card() end do - {:error, %Response{reason: reason}} = - Gateway.authorize(@money, @bad_card, @options) + {:error, %Response{reason: reason}} = Gateway.authorize(@money, @bad_card, @options) assert String.contains?(reason, "Invalid Credit Card Number") end @@ -106,23 +104,20 @@ defmodule Gringotts.Gateways.CamsTest do describe "capture" do test "with full amount" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_capture() end do - assert {:ok, %Response{}} = - Gateway.capture(@money, @id , @options) + assert {:ok, %Response{}} = Gateway.capture(@money, @id, @options) end end test "with partial amount" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_capture() end do - assert {:ok, %Response{}} = - Gateway.capture(@money_less, @id , @options) + assert {:ok, %Response{}} = Gateway.capture(@money_less, @id, @options) end end test "with invalid transaction_id" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.invalid_transaction_id() end do - {:error, %Response{reason: reason}} = - Gateway.capture(@money, @bad_id, @options) + {:error, %Response{reason: reason}} = Gateway.capture(@money, @bad_id, @options) assert String.contains?(reason, "Transaction not found") end @@ -131,8 +126,7 @@ defmodule Gringotts.Gateways.CamsTest do test "with more than authorized amount" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.more_than_authorization_amount() end do - {:error, %Response{reason: reason}} = - Gateway.capture(@money_more, @id , @options) + {:error, %Response{reason: reason}} = Gateway.capture(@money_more, @id, @options) assert String.contains?(reason, "exceeds the authorization amount") end @@ -141,8 +135,7 @@ defmodule Gringotts.Gateways.CamsTest do test "on already captured transaction" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.multiple_capture_on_same_transaction() end do - {:error, %Response{reason: reason}} = - Gateway.capture(@money, @id , @options) + {:error, %Response{reason: reason}} = Gateway.capture(@money, @id, @options) assert String.contains?(reason, "A capture requires that") end @@ -152,16 +145,14 @@ defmodule Gringotts.Gateways.CamsTest do describe "refund" do test "with correct params" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_refund() end do - assert {:ok, %Response{}} = - Gateway.refund(@money, @id , @options) + assert {:ok, %Response{}} = Gateway.refund(@money, @id, @options) end end test "with more than purchased amount" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.more_than_purchase_amount() end do - {:error, %Response{reason: reason}} = - Gateway.refund(@money_more, @id , @options) + {:error, %Response{reason: reason}} = Gateway.refund(@money_more, @id, @options) assert String.contains?(reason, "Refund amount may not exceed") end @@ -171,7 +162,7 @@ defmodule Gringotts.Gateways.CamsTest do describe "void" do test "with correct params" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_void() end do - {:ok, %Response{message: message}} = Gateway.void(@id , @options) + {:ok, %Response{message: message}} = Gateway.void(@id, @options) assert String.contains?(message, "Void Successful") end end diff --git a/test/gateways/global_collect_test.exs b/test/gateways/global_collect_test.exs index 87b0d702..d64489c2 100644 --- a/test/gateways/global_collect_test.exs +++ b/test/gateways/global_collect_test.exs @@ -1,9 +1,9 @@ defmodule Gringotts.Gateways.GlobalCollectTest do - - Code.require_file "../mocks/global_collect_mock.exs", __DIR__ + Code.require_file("../mocks/global_collect_mock.exs", __DIR__) use ExUnit.Case, async: false alias Gringotts.Gateways.GlobalCollectMock, as: MockResponse alias Gringotts.Gateways.GlobalCollect + alias Gringotts.{ CreditCard } @@ -69,21 +69,32 @@ defmodule Gringotts.Gateways.GlobalCollectTest do @invalid_token 30 - @invalid_config [config: %{secret_api_key: "Qtg9v4Q0G13sLRNcClWhHnvN1kVYWDcy4w9rG8T86XU=", api_key_id: "e5743abfc360ed12"}] + @invalid_config [ + config: %{ + secret_api_key: "Qtg9v4Q0G13sLRNcClWhHnvN1kVYWDcy4w9rG8T86XU=", + api_key_id: "e5743abfc360ed12" + } + ] @options [ - config: %{secret_api_key: "Qtg9v4Q0G13sLRNcClWhHnvN1kVYWDcy4w9rG8T86XU=", api_key_id: "e5743abfc360ed12", merchant_id: "1226"}, + config: %{ + secret_api_key: "Qtg9v4Q0G13sLRNcClWhHnvN1kVYWDcy4w9rG8T86XU=", + api_key_id: "e5743abfc360ed12", + merchant_id: "1226" + }, description: "Store Purchase 1437598192", merchantCustomerId: "234", customer_name: "John Doe", - dob: "19490917", company: "asma", + dob: "19490917", + company: "asma", email: "johndoe@gmail.com", phone: "7468474533", order_id: "2323", invoice: @invoice, billingAddress: @billingAddress, shippingAddress: @shippingAddress, - name: @name, skipAuthentication: "true" + name: @name, + skipAuthentication: "true" ] describe "validation arguments check" do @@ -97,18 +108,21 @@ defmodule Gringotts.Gateways.GlobalCollectTest do describe "purchase" do test "with valid card" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_purchase_with_valid_card end] do - {:ok, response} = GlobalCollect.purchase(@amount, @valid_card, @options) - assert response.status_code == 201 - assert response.success == true - assert response.raw["payment"]["statusOutput"]["isAuthorized"] == true + request: fn _method, _url, _body, _headers -> + MockResponse.test_for_purchase_with_valid_card() + end do + {:ok, response} = GlobalCollect.purchase(@amount, @valid_card, @options) + assert response.status_code == 201 + assert response.success == true + assert response.raw["payment"]["statusOutput"]["isAuthorized"] == true end end - test "with invalid amount" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_purchase_with_invalid_amount end] do + request: fn _method, _url, _body, _headers -> + MockResponse.test_for_purchase_with_invalid_amount() + end do {:error, response} = GlobalCollect.purchase(@bad_amount, @valid_card, @options) assert response.status_code == 400 assert response.success == false @@ -120,7 +134,9 @@ defmodule Gringotts.Gateways.GlobalCollectTest do describe "authorize" do test "with valid card" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_authorize_with_valid_card end] do + request: fn _method, _url, _body, _headers -> + MockResponse.test_for_authorize_with_valid_card() + end do {:ok, response} = GlobalCollect.authorize(@amount, @valid_card, @options) assert response.status_code == 201 assert response.success == true @@ -130,17 +146,23 @@ defmodule Gringotts.Gateways.GlobalCollectTest do test "with invalid card" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_authorize_with_invalid_card end] do + request: fn _method, _url, _body, _headers -> + MockResponse.test_for_authorize_with_invalid_card() + end do {:error, response} = GlobalCollect.authorize(@amount, @invalid_card, @options) assert response.status_code == 400 assert response.success == false - assert response.message == "cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT" + + assert response.message == + "cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT" end end test "with invalid amount" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_authorize_with_invalid_amount end] do + request: fn _method, _url, _body, _headers -> + MockResponse.test_for_authorize_with_invalid_amount() + end do {:error, response} = GlobalCollect.authorize(@bad_amount, @valid_card, @options) assert response.status_code == 400 assert response.success == false @@ -152,7 +174,7 @@ defmodule Gringotts.Gateways.GlobalCollectTest do describe "refund" do test "with refund not enabled for the respective account" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_refund end] do + request: fn _method, _url, _body, _headers -> MockResponse.test_for_refund() end do {:error, response} = GlobalCollect.refund(@amount, @valid_token, @options) assert response.status_code == 400 assert response.success == false @@ -164,7 +186,9 @@ defmodule Gringotts.Gateways.GlobalCollectTest do describe "capture" do test "with valid payment id" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_capture_with_valid_paymentid end] do + request: fn _method, _url, _body, _headers -> + MockResponse.test_for_capture_with_valid_paymentid() + end do {:ok, response} = GlobalCollect.capture(@valid_token, @amount, @options) assert response.status_code == 200 assert response.success == true @@ -173,20 +197,24 @@ defmodule Gringotts.Gateways.GlobalCollectTest do end test "with invalid payment id" do - with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_capture_with_invalid_paymentid end] do - {:error, response} = GlobalCollect.capture(@invalid_token, @amount, @options) - assert response.status_code == 404 - assert response.success == false - assert response.message == "UNKNOWN_PAYMENT_ID" - end + with_mock HTTPoison, + request: fn _method, _url, _body, _headers -> + MockResponse.test_for_capture_with_invalid_paymentid() + end do + {:error, response} = GlobalCollect.capture(@invalid_token, @amount, @options) + assert response.status_code == 404 + assert response.success == false + assert response.message == "UNKNOWN_PAYMENT_ID" + end end end describe "void" do test "with valid card" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_void_with_valid_card end] do + request: fn _method, _url, _body, _headers -> + MockResponse.test_for_void_with_valid_card() + end do {:ok, response} = GlobalCollect.void(@valid_token, @options) assert response.status_code == 200 assert response.raw["payment"]["status"] == "CANCELLED" @@ -197,7 +225,7 @@ defmodule Gringotts.Gateways.GlobalCollectTest do describe "network failure" do test "with authorization" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_network_failure end] do + request: fn _method, _url, _body, _headers -> MockResponse.test_for_network_failure() end do {:error, response} = GlobalCollect.authorize(@amount, @valid_card, @options) assert response.success == false assert response.reason == :network_fail? diff --git a/test/gateways/monei_test.exs b/test/gateways/monei_test.exs index ae41e9a0..e641d91b 100644 --- a/test/gateways/monei_test.exs +++ b/test/gateways/monei_test.exs @@ -6,6 +6,7 @@ defmodule Gringotts.Gateways.MoneiTest do } alias Gringotts.Gateways.Monei, as: Gateway + alias Plug.{Conn, Parsers} @amount42 Money.new(42, :USD) @amount3 Money.new(3, :USD) @@ -136,7 +137,7 @@ defmodule Gringotts.Gateways.MoneiTest do assert params["authentication.entityId"] == "some_secret_entity_id" assert params["authentication.password"] == "some_secret_password" assert params["authentication.userId"] == "some_secret_user_id" - Plug.Conn.resp(conn, 200, @auth_success) + Conn.resp(conn, 200, @auth_success) end) {:ok, response} = Gateway.purchase(@amount42, @card, config: auth) @@ -159,13 +160,12 @@ defmodule Gringotts.Gateways.MoneiTest do assert params["transactionCategory"] == @extra_opts[:category] assert params["customer.merchantCustomerId"] == @customer[:merchantCustomerId] - assert params["shipping.customer.merchantCustomerId"] == - @customer[:merchantCustomerId] + assert params["shipping.customer.merchantCustomerId"] == @customer[:merchantCustomerId] assert params["merchant.submerchantId"] == @merchant[:submerchantId] assert params["billing.city"] == @billing[:city] assert params["shipping.method"] == @shipping[:method] - Plug.Conn.resp(conn, 200, @register_success) + Conn.resp(conn, 200, @register_success) end) opts = randoms ++ @extra_opts ++ [config: auth] @@ -176,7 +176,7 @@ defmodule Gringotts.Gateways.MoneiTest do test "when we get non-json.", %{bypass: bypass, auth: auth} do Bypass.expect_once(bypass, "POST", "/v1/payments", fn conn -> - Plug.Conn.resp(conn, 400, "") + Conn.resp(conn, 400, "") end) {:error, _} = Gateway.authorize(@amount42, @bad_card, config: auth) @@ -191,7 +191,7 @@ defmodule Gringotts.Gateways.MoneiTest do assert params["amount"] == "42.00" assert params["currency"] == "USD" assert params["paymentType"] == "PA" - Plug.Conn.resp(conn, 200, @auth_success) + Conn.resp(conn, 200, @auth_success) end) {:ok, response} = Gateway.authorize(@amount42, @card, config: auth) @@ -207,7 +207,7 @@ defmodule Gringotts.Gateways.MoneiTest do assert params["amount"] == "42.00" assert params["currency"] == "USD" assert params["paymentType"] == "DB" - Plug.Conn.resp(conn, 200, @auth_success) + Conn.resp(conn, 200, @auth_success) end) {:ok, response} = Gateway.purchase(@amount42, @card, config: auth) @@ -219,7 +219,7 @@ defmodule Gringotts.Gateways.MoneiTest do p_conn = parse(conn) params = p_conn.body_params assert params["createRegistration"] == "true" - Plug.Conn.resp(conn, 200, @register_success) + Conn.resp(conn, 200, @register_success) end) {:ok, response} = Gateway.purchase(@amount42, @card, register: true, config: auth) @@ -239,7 +239,7 @@ defmodule Gringotts.Gateways.MoneiTest do assert params["card.holder"] == "Harry Potter" assert params["card.number"] == "4200000000000000" assert params["paymentBrand"] == "VISA" - Plug.Conn.resp(conn, 200, @store_success) + Conn.resp(conn, 200, @store_success) end) {:ok, response} = Gateway.store(@card, config: auth) @@ -259,7 +259,7 @@ defmodule Gringotts.Gateways.MoneiTest do assert params["amount"] == "42.00" assert params["currency"] == "USD" assert params["paymentType"] == "CP" - Plug.Conn.resp(conn, 200, @auth_success) + Conn.resp(conn, 200, @auth_success) end ) @@ -278,7 +278,7 @@ defmodule Gringotts.Gateways.MoneiTest do p_conn = parse(conn) params = p_conn.body_params assert :error == Map.fetch(params, "createRegistration") - Plug.Conn.resp(conn, 200, @auth_success) + Conn.resp(conn, 200, @auth_success) end ) @@ -306,7 +306,7 @@ defmodule Gringotts.Gateways.MoneiTest do assert params["amount"] == "3.00" assert params["currency"] == "USD" assert params["paymentType"] == "RF" - Plug.Conn.resp(conn, 200, @auth_success) + Conn.resp(conn, 200, @auth_success) end ) @@ -328,7 +328,7 @@ defmodule Gringotts.Gateways.MoneiTest do assert params["authentication.entityId"] == "some_secret_entity_id" assert params["authentication.password"] == "some_secret_password" assert params["authentication.userId"] == "some_secret_user_id" - Plug.Conn.resp(conn, 200, "") + Conn.resp(conn, 200, "") end ) @@ -349,7 +349,7 @@ defmodule Gringotts.Gateways.MoneiTest do assert :error == Map.fetch(params, :amount) assert :error == Map.fetch(params, :currency) assert params["paymentType"] == "RV" - Plug.Conn.resp(conn, 200, @auth_success) + Conn.resp(conn, 200, @auth_success) end ) @@ -359,8 +359,8 @@ defmodule Gringotts.Gateways.MoneiTest do end def parse(conn, opts \\ []) do - opts = Keyword.put_new(opts, :parsers, [Plug.Parsers.URLENCODED]) - Plug.Parsers.call(conn, Plug.Parsers.init(opts)) + opts = Keyword.put_new(opts, :parsers, [Parsers.URLENCODED]) + Parsers.call(conn, Parsers.init(opts)) end end diff --git a/test/gateways/trexle_test.exs b/test/gateways/trexle_test.exs index 7078b9f6..00da54ca 100644 --- a/test/gateways/trexle_test.exs +++ b/test/gateways/trexle_test.exs @@ -157,7 +157,9 @@ defmodule Gringotts.Gateways.TrexleTest do MockResponse.test_for_network_failure() end do {:error, response} = Trexle.authorize(@amount, @valid_card, @opts) - assert response.message == "HTTPoison says 'some_hackney_error' [ID: some_hackney_error_id]" + + assert response.message == + "HTTPoison says 'some_hackney_error' [ID: some_hackney_error_id]" end end end diff --git a/test/gateways/wire_card_test.exs b/test/gateways/wire_card_test.exs index d23d9090..59de0f03 100644 --- a/test/gateways/wire_card_test.exs +++ b/test/gateways/wire_card_test.exs @@ -3,18 +3,11 @@ defmodule Gringotts.Gateways.WireCardTest do import Mock - alias Gringotts.{ - CreditCard, - Address, - Response - } - alias Gringotts.Gateways.WireCard, as: Gateway - setup do # TEST_AUTHORIZATION_GUWID = 'C822580121385121429927' # TEST_PURCHASE_GUWID = 'C865402121385575982910' # TEST_CAPTURE_GUWID = 'C833707121385268439116' - + # credit_card = %CreditCard{name: "Longbob", number: "4200000000000000", cvc: "123", expiration: {2015, 11}} # config = %{credentails: {'user', 'pass'}, default_currency: "EUR"} @@ -24,7 +17,4 @@ defmodule Gringotts.Gateways.WireCardTest do test "test_successful_authorization" do assert 1 + 1 == 2 end - - - end diff --git a/test/gringotts_test.exs b/test/gringotts_test.exs index 7d9f7681..01d01824 100644 --- a/test/gringotts_test.exs +++ b/test/gringotts_test.exs @@ -48,13 +48,11 @@ defmodule GringottsTest do end test "authorization" do - assert authorize(GringottsTest.FakeGateway, 100, :card, []) == - :authorization_response + assert authorize(GringottsTest.FakeGateway, 100, :card, []) == :authorization_response end test "purchase" do - assert purchase(GringottsTest.FakeGateway, 100, :card, []) == - :purchase_response + assert purchase(GringottsTest.FakeGateway, 100, :card, []) == :purchase_response end test "capture" do diff --git a/test/integration/gateways/monei_test.exs b/test/integration/gateways/monei_test.exs index 71f48bd7..59bc9b88 100644 --- a/test/integration/gateways/monei_test.exs +++ b/test/integration/gateways/monei_test.exs @@ -68,7 +68,7 @@ defmodule Gringotts.Integration.Gateways.MoneiTest do password: "hMkqf2qbWf", entityId: "8a82941760036820016010a28a8337f6" } - + setup_all do Application.put_env( :gringotts, @@ -104,7 +104,8 @@ defmodule Gringotts.Integration.Gateways.MoneiTest do {:ok, _registration_token} <- Map.fetch(auth_result, :token), {:ok, _capture_result} <- Gateway.capture(auth_result.id, @amount, opts) do "yay!" - else {:error, _err} -> + else + {:error, _err} -> flunk() end end @@ -113,7 +114,8 @@ defmodule Gringotts.Integration.Gateways.MoneiTest do with {:ok, auth_result} <- Gateway.authorize(@amount, @card, opts), {:ok, _void_result} <- Gateway.void(auth_result.id, opts) do "yay!" - else {:error, _err} -> + else + {:error, _err} -> flunk() end end @@ -122,7 +124,8 @@ defmodule Gringotts.Integration.Gateways.MoneiTest do with {:ok, purchase_result} <- Gateway.purchase(@amount, @card, opts), {:ok, _void_result} <- Gateway.void(purchase_result.id, opts) do "yay!" - else {:error, _err} -> + else + {:error, _err} -> flunk() end end @@ -131,7 +134,8 @@ defmodule Gringotts.Integration.Gateways.MoneiTest do with {:ok, purchase_result} <- Gateway.purchase(@amount, @card, opts), {:ok, _refund_result} <- Gateway.refund(@sub_amount, purchase_result.id, opts) do "yay!" - else {:error, _err} -> + else + {:error, _err} -> flunk() end end @@ -145,7 +149,8 @@ defmodule Gringotts.Integration.Gateways.MoneiTest do with {:ok, store_result} <- Gateway.store(@card, opts), {:ok, _unstore_result} <- Gateway.unstore(store_result.id, opts) do "yay!" - else {:error, _err} -> + else + {:error, _err} -> flunk() end end diff --git a/test/integration/gateways/stripe_test.exs b/test/integration/gateways/stripe_test.exs index b390f989..e4d3e070 100644 --- a/test/integration/gateways/stripe_test.exs +++ b/test/integration/gateways/stripe_test.exs @@ -1,21 +1,22 @@ defmodule Gringotts.Gateways.StripeTest do - use ExUnit.Case alias Gringotts.Gateways.Stripe + alias Gringotts.{ CreditCard, Address } @moduletag integration: true - + @amount Money.new(5, :USD) @card %CreditCard{ first_name: "John", last_name: "Smith", number: "4242424242424242", - year: "2068", # Can't be more than 50 years in the future, Haha. + # Can't be more than 50 years in the future, Haha. + year: "2068", month: "12", verification_code: "123" } diff --git a/test/integration/money.exs b/test/integration/money.exs index 3f5691ba..e4aaa331 100644 --- a/test/integration/money.exs +++ b/test/integration/money.exs @@ -4,7 +4,7 @@ defmodule Gringotts.Integration.Gateways.MoneyTest do alias Gringotts.Money, as: MoneyProtocol @moduletag :integration - + @ex_money Money.new(42, :EUR) @ex_money_long Money.new("42.126456", :EUR) @ex_money_bhd Money.new(42, :BHD) @@ -12,50 +12,50 @@ defmodule Gringotts.Integration.Gateways.MoneyTest do @any %{value: Decimal.new(42), currency: "EUR"} @any_long %{value: Decimal.new("42.126456"), currency: "EUR"} @any_bhd %{value: Decimal.new("42"), currency: "BHD"} - + describe "ex_money" do test "value is a Decimal.t" do - assert match? %Decimal{}, MoneyProtocol.value(@ex_money) + assert match?(%Decimal{}, MoneyProtocol.value(@ex_money)) end test "currency is an upcase String.t" do the_currency = MoneyProtocol.currency(@ex_money) - assert match? currency when is_binary(currency), the_currency + assert match?(currency when is_binary(currency), the_currency) assert the_currency == String.upcase(the_currency) end test "to_integer" do - assert match? {"EUR", 4200, -2}, MoneyProtocol.to_integer(@ex_money) - assert match? {"BHD", 42_000, -3}, MoneyProtocol.to_integer(@ex_money_bhd) + assert match?({"EUR", 4200, -2}, MoneyProtocol.to_integer(@ex_money)) + assert match?({"BHD", 42_000, -3}, MoneyProtocol.to_integer(@ex_money_bhd)) end test "to_string" do - assert match? {"EUR", "42.00"}, MoneyProtocol.to_string(@ex_money) - assert match? {"EUR", "42.13"}, MoneyProtocol.to_string(@ex_money_long) - assert match? {"BHD", "42.000"}, MoneyProtocol.to_string(@ex_money_bhd) + assert match?({"EUR", "42.00"}, MoneyProtocol.to_string(@ex_money)) + assert match?({"EUR", "42.13"}, MoneyProtocol.to_string(@ex_money_long)) + assert match?({"BHD", "42.000"}, MoneyProtocol.to_string(@ex_money_bhd)) end end describe "Any" do test "value is a Decimal.t" do - assert match? %Decimal{}, MoneyProtocol.value(@any) + assert match?(%Decimal{}, MoneyProtocol.value(@any)) end test "currency is an upcase String.t" do the_currency = MoneyProtocol.currency(@any) - assert match? currency when is_binary(currency), the_currency + assert match?(currency when is_binary(currency), the_currency) assert the_currency == String.upcase(the_currency) end test "to_integer" do - assert match? {"EUR", 4200, -2}, MoneyProtocol.to_integer(@any) - assert match? {"BHD", 4200, -2}, MoneyProtocol.to_integer(@any_bhd) + assert match?({"EUR", 4200, -2}, MoneyProtocol.to_integer(@any)) + assert match?({"BHD", 4200, -2}, MoneyProtocol.to_integer(@any_bhd)) end test "to_string" do - assert match? {"EUR", "42.00"}, MoneyProtocol.to_string(@any) - assert match? {"EUR", "42.13"}, MoneyProtocol.to_string(@any_long) - assert match? {"BHD", "42.00"}, MoneyProtocol.to_string(@any_bhd) + assert match?({"EUR", "42.00"}, MoneyProtocol.to_string(@any)) + assert match?({"EUR", "42.13"}, MoneyProtocol.to_string(@any_long)) + assert match?({"BHD", "42.00"}, MoneyProtocol.to_string(@any_bhd)) end end end diff --git a/test/mocks/authorize_net_mock.exs b/test/mocks/authorize_net_mock.exs index cb68df4a..712602cd 100644 --- a/test/mocks/authorize_net_mock.exs +++ b/test/mocks/authorize_net_mock.exs @@ -1,321 +1,422 @@ - defmodule Gringotts.Gateways.AuthorizeNetMock do - - # purchase mock response - def successful_purchase_response do - {:ok, - %HTTPoison.Response{body: ~s{123456OkI00001Successful.1C7HPT1YP2600365530965D6782A03246EE3BAFABE8006E32DE970XXXX0015MasterCard1This transaction has been approved.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-13182173"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "908"}, {"Date", "Thu, 21 Dec 2017 09:29:12 GMT"}, - {"Connection", "keep-alive"}], +defmodule Gringotts.Gateways.AuthorizeNetMock do + # purchase mock response + def successful_purchase_response do + {:ok, + %HTTPoison.Response{ + body: + ~s{123456OkI00001Successful.1C7HPT1YP2600365530965D6782A03246EE3BAFABE8006E32DE970XXXX0015MasterCard1This transaction has been approved.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-13182173"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "908"}, + {"Date", "Thu, 21 Dec 2017 09:29:12 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end + status_code: 200 + }} + end - def bad_card_purchase_response do - {:ok, - %HTTPoison.Response{body: ~s{ErrorE00003The 'AnetApi/xml/v1/schema/AnetApiSchema.xsd:cardNumber' element is invalid - The value XXXXX is invalid according to its datatype 'String' - The actual length is less than the MinLength value.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_f2f80544-1a98-4ad7-989b-8d267ebf5043-56152-10066531"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "514"}, {"Date", "Thu, 21 Dec 2017 09:35:45 GMT"}, - {"Connection", "keep-alive"}], + def bad_card_purchase_response do + {:ok, + %HTTPoison.Response{ + body: + ~s{ErrorE00003The 'AnetApi/xml/v1/schema/AnetApiSchema.xsd:cardNumber' element is invalid - The value XXXXX is invalid according to its datatype 'String' - The actual length is less than the MinLength value.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_f2f80544-1a98-4ad7-989b-8d267ebf5043-56152-10066531"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "514"}, + {"Date", "Thu, 21 Dec 2017 09:35:45 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end + status_code: 200 + }} + end - def bad_amount_purchase_response do - {:ok, - %HTTPoison.Response{body: ~s{123456ErrorE00027The transaction was unsuccessful.3P0C7C56F020A2AE2660A87637CD00B4D5C0XXXX0015MasterCard5A valid amount is required.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-13187900"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "867"}, {"Date", "Thu, 21 Dec 2017 09:44:33 GMT"}, - {"Connection", "keep-alive"}], + def bad_amount_purchase_response do + {:ok, + %HTTPoison.Response{ + body: + ~s{123456ErrorE00027The transaction was unsuccessful.3P0C7C56F020A2AE2660A87637CD00B4D5C0XXXX0015MasterCard5A valid amount is required.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-13187900"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "867"}, + {"Date", "Thu, 21 Dec 2017 09:44:33 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end + status_code: 200 + }} + end - # authorize mock response - def successful_authorize_response do - {:ok, - %HTTPoison.Response{body: ~s{123456OkI00001Successful.1K6Z0ABYP260036854582A4AD079E22A271D92662CF093CED7A5D0XXXX0015MasterCard1This transaction has been approved.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15778237"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "908"}, {"Date", "Mon, 25 Dec 2017 14:17:56 GMT"}, - {"Connection", "keep-alive"}], + # authorize mock response + def successful_authorize_response do + {:ok, + %HTTPoison.Response{ + body: + ~s{123456OkI00001Successful.1K6Z0ABYP260036854582A4AD079E22A271D92662CF093CED7A5D0XXXX0015MasterCard1This transaction has been approved.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15778237"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "908"}, + {"Date", "Mon, 25 Dec 2017 14:17:56 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end + status_code: 200 + }} + end - def bad_card_authorize_response do - {:ok, - %HTTPoison.Response{body: ~s{ErrorE00003The 'AnetApi/xml/v1/schema/AnetApiSchema.xsd:cardNumber' element is invalid - The value XXXXX is invalid according to its datatype 'String' - The actual length is less than the MinLength value.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_f2f80544-1a98-4ad7-989b-8d267ebf5043-56152-12660528"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "514"}, {"Date", "Mon, 25 Dec 2017 14:19:29 GMT"}, - {"Connection", "keep-alive"}], + def bad_card_authorize_response do + {:ok, + %HTTPoison.Response{ + body: + ~s{ErrorE00003The 'AnetApi/xml/v1/schema/AnetApiSchema.xsd:cardNumber' element is invalid - The value XXXXX is invalid according to its datatype 'String' - The actual length is less than the MinLength value.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_f2f80544-1a98-4ad7-989b-8d267ebf5043-56152-12660528"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "514"}, + {"Date", "Mon, 25 Dec 2017 14:19:29 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end + status_code: 200 + }} + end - def bad_amount_authorize_response do - {:ok, - %HTTPoison.Response{body: ~s{123456ErrorE00027The transaction was unsuccessful.3P0C7C56F020A2AE2660A87637CD00B4D5C0XXXX0015MasterCard290There is one or more missing or invalid required fields.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15779095"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "898"}, {"Date", "Mon, 25 Dec 2017 14:22:02 GMT"}, - {"Connection", "keep-alive"}], + def bad_amount_authorize_response do + {:ok, + %HTTPoison.Response{ + body: + ~s{123456ErrorE00027The transaction was unsuccessful.3P0C7C56F020A2AE2660A87637CD00B4D5C0XXXX0015MasterCard290There is one or more missing or invalid required fields.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15779095"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "898"}, + {"Date", "Mon, 25 Dec 2017 14:22:02 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end + status_code: 200 + }} + end - # capture mock response + # capture mock response - def successful_capture_response do - {:ok, - %HTTPoison.Response{body: ~s{123456OkI00001Successful.14OKD6YP6003685493160036854931348C4ECD0F764736B012C4655BFA68EF0XXXX0015MasterCard1This transaction has been approved.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15783402"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "899"}, {"Date", "Mon, 25 Dec 2017 14:39:28 GMT"}, - {"Connection", "keep-alive"}], + def successful_capture_response do + {:ok, + %HTTPoison.Response{ + body: + ~s{123456OkI00001Successful.14OKD6YP6003685493160036854931348C4ECD0F764736B012C4655BFA68EF0XXXX0015MasterCard1This transaction has been approved.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15783402"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "899"}, + {"Date", "Mon, 25 Dec 2017 14:39:28 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end + status_code: 200 + }} + end - def bad_id_capture do - {:ok, - %HTTPoison.Response{body: ~s{123456ErrorE00027The transaction was unsuccessful.3P0A5280E2A6AA1290D451A24286692D1B0033A valid referenced transaction ID is required.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15784805"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "843"}, {"Date", "Mon, 25 Dec 2017 14:45:32 GMT"}, - {"Connection", "keep-alive"}], + def bad_id_capture do + {:ok, + %HTTPoison.Response{ + body: + ~s{123456ErrorE00027The transaction was unsuccessful.3P0A5280E2A6AA1290D451A24286692D1B0033A valid referenced transaction ID is required.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15784805"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "843"}, + {"Date", "Mon, 25 Dec 2017 14:45:32 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end + status_code: 200 + }} + end - # refund mock response - def successful_refund_response do - {:ok, - %HTTPoison.Response{body: ~s{123456OkI00001Successful.1P6003685566160036752756169F2381B172A5AA247A01757A3E520A0XXXX0015MasterCard1This transaction has been approved.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_f2f80544-1a98-4ad7-989b-8d267ebf5043-56152-12678232"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "884"}, {"Date", "Mon, 25 Dec 2017 15:22:19 GMT"}, - {"Connection", "keep-alive"}], + # refund mock response + def successful_refund_response do + {:ok, + %HTTPoison.Response{ + body: + ~s{123456OkI00001Successful.1P6003685566160036752756169F2381B172A5AA247A01757A3E520A0XXXX0015MasterCard1This transaction has been approved.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_f2f80544-1a98-4ad7-989b-8d267ebf5043-56152-12678232"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "884"}, + {"Date", "Mon, 25 Dec 2017 15:22:19 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end + status_code: 200 + }} + end - def bad_card_refund do - {:ok, - %HTTPoison.Response{body: ~s{ErrorE00003The 'AnetApi/xml/v1/schema/AnetApiSchema.xsd:cardNumber' element is invalid - The value XX is invalid according to its datatype 'String' - The actual length is less than the MinLength value.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15795999"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "511"}, {"Date", "Mon, 25 Dec 2017 15:21:20 GMT"}, - {"Connection", "keep-alive"}], + def bad_card_refund do + {:ok, + %HTTPoison.Response{ + body: + ~s{ErrorE00003The 'AnetApi/xml/v1/schema/AnetApiSchema.xsd:cardNumber' element is invalid - The value XX is invalid according to its datatype 'String' - The actual length is less than the MinLength value.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15795999"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "511"}, + {"Date", "Mon, 25 Dec 2017 15:21:20 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end + status_code: 200 + }} + end - def debit_less_than_refund do - {:ok, - %HTTPoison.Response{body: ~s{123456ErrorE00027The transaction was unsuccessful.3P060036752756A5280E2A6AA1290D451A24286692D1B00XXXX0015MasterCard55The sum of credits against the referenced transaction would exceed original debit amount.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_f2f80544-1a98-4ad7-989b-8d267ebf5043-56152-12681460"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "952"}, {"Date", "Mon, 25 Dec 2017 15:39:25 GMT"}, - {"Connection", "keep-alive"}], + def debit_less_than_refund do + {:ok, + %HTTPoison.Response{ + body: + ~s{123456ErrorE00027The transaction was unsuccessful.3P060036752756A5280E2A6AA1290D451A24286692D1B00XXXX0015MasterCard55The sum of credits against the referenced transaction would exceed original debit amount.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_f2f80544-1a98-4ad7-989b-8d267ebf5043-56152-12681460"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "952"}, + {"Date", "Mon, 25 Dec 2017 15:39:25 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end + status_code: 200 + }} + end - # void mock response - def successful_void do - {:ok, - %HTTPoison.Response{body: ~s{123456OkI00001Successful.1ZJPVRXP6003685521760036855217F09A215511891DCEA91B6CC52B9F4E870XXXX0015MasterCard1This transaction has been approved.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_f2f80544-1a98-4ad7-989b-8d267ebf5043-56152-12682366"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "899"}, {"Date", "Mon, 25 Dec 2017 15:43:56 GMT"}, - {"Connection", "keep-alive"}], + # void mock response + def successful_void do + {:ok, + %HTTPoison.Response{ + body: + ~s{123456OkI00001Successful.1ZJPVRXP6003685521760036855217F09A215511891DCEA91B6CC52B9F4E870XXXX0015MasterCard1This transaction has been approved.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_f2f80544-1a98-4ad7-989b-8d267ebf5043-56152-12682366"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "899"}, + {"Date", "Mon, 25 Dec 2017 15:43:56 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end + status_code: 200 + }} + end - def void_non_existent_id do - {:ok, - %HTTPoison.Response{body: ~s{123456ErrorE00027The transaction was unsuccessful.3P060036855219C7C56F020A2AE2660A87637CD00B4D5C016The transaction cannot be found.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15801470"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "861"}, {"Date", "Mon, 25 Dec 2017 15:49:38 GMT"}, - {"Connection", "keep-alive"}], + def void_non_existent_id do + {:ok, + %HTTPoison.Response{ + body: + ~s{123456ErrorE00027The transaction was unsuccessful.3P060036855219C7C56F020A2AE2660A87637CD00B4D5C016The transaction cannot be found.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15801470"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "861"}, + {"Date", "Mon, 25 Dec 2017 15:49:38 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end + status_code: 200 + }} + end - # store mock response + # store mock response - def successful_store_response do - {:ok, - %HTTPoison.Response{body: ~s{OkI00001Successful.18139914901808649724}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15829721"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "577"}, {"Date", "Mon, 25 Dec 2017 17:08:12 GMT"}, - {"Connection", "keep-alive"}], + def successful_store_response do + {:ok, + %HTTPoison.Response{ + body: + ~s{OkI00001Successful.18139914901808649724}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15829721"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "577"}, + {"Date", "Mon, 25 Dec 2017 17:08:12 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end - - def store_without_profile_fields do - {:ok, - %HTTPoison.Response{body: ~s{ErrorE00041One or more fields in the profile must contain a value.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15831457"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "408"}, {"Date", "Mon, 25 Dec 2017 17:12:30 GMT"}, - {"Connection", "keep-alive"}], + status_code: 200 + }} + end + + def store_without_profile_fields do + {:ok, + %HTTPoison.Response{ + body: + ~s{ErrorE00041One or more fields in the profile must contain a value.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15831457"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "408"}, + {"Date", "Mon, 25 Dec 2017 17:12:30 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end + status_code: 200 + }} + end - #unstore mock response - def successful_unstore_response do - {:ok, - %HTTPoison.Response{body: ~s{OkI00001Successful.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15833786"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "361"}, {"Date", "Mon, 25 Dec 2017 17:21:20 GMT"}, - {"Connection", "keep-alive"}], + # unstore mock response + def successful_unstore_response do + {:ok, + %HTTPoison.Response{ + body: + ~s{OkI00001Successful.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15833786"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "361"}, + {"Date", "Mon, 25 Dec 2017 17:21:20 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end + status_code: 200 + }} + end - def customer_payment_profile_success_response do - {:ok, - %HTTPoison.Response{body: ~s{OkI00001Successful.181401200218086700051,1,1,(TESTMODE) This transaction has been approved.,000000,P,0,none,Test transaction for ValidateCustomerPaymentProfile.,1.00,CC,auth_only,none,,,,,,,,,,,email@example.com,,,,,,,,,0.00,0.00,0.00,FALSE,none,EA9FD49A9501D0415FE26BAEF9FD8B2C,,,,,,,,,,,,,XXXX0015,MasterCard,,,,,,,,,,,,,,,,,}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-17537805"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "828"}, {"Date", "Thu, 28 Dec 2017 13:54:20 GMT"}, - {"Connection", "keep-alive"}], + def customer_payment_profile_success_response do + {:ok, + %HTTPoison.Response{ + body: + ~s{OkI00001Successful.181401200218086700051,1,1,(TESTMODE) This transaction has been approved.,000000,P,0,none,Test transaction for ValidateCustomerPaymentProfile.,1.00,CC,auth_only,none,,,,,,,,,,,email@example.com,,,,,,,,,0.00,0.00,0.00,FALSE,none,EA9FD49A9501D0415FE26BAEF9FD8B2C,,,,,,,,,,,,,XXXX0015,MasterCard,,,,,,,,,,,,,,,,,}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-17537805"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "828"}, + {"Date", "Thu, 28 Dec 2017 13:54:20 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end - - def netwok_error_non_existent_domain do - {:error, %HTTPoison.Error{id: nil, reason: :nxdomain}} - end + status_code: 200 + }} + end + + def netwok_error_non_existent_domain do + {:error, %HTTPoison.Error{id: nil, reason: :nxdomain}} end +end diff --git a/test/mocks/cams_mock.exs b/test/mocks/cams_mock.exs index e499b450..1eb50faf 100644 --- a/test/mocks/cams_mock.exs +++ b/test/mocks/cams_mock.exs @@ -1,211 +1,225 @@ defmodule Gringotts.Gateways.CamsMock do def successful_purchase do - {:ok, %HTTPoison.Response{ - body: - "response=1&responsetext=SUCCESS&authcode=123456&transactionid=3916017714&avsresponse=N&cvvresponse=N&orderid=&type=sale&response_code=100", - headers: [ - {"Date", "Thu, 21 Dec 2017 12:45:16 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "137"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} + {:ok, + %HTTPoison.Response{ + body: + "response=1&responsetext=SUCCESS&authcode=123456&transactionid=3916017714&avsresponse=N&cvvresponse=N&orderid=&type=sale&response_code=100", + headers: [ + {"Date", "Thu, 21 Dec 2017 12:45:16 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "137"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end def failed_purchase_with_bad_credit_card do - {:ok, %HTTPoison.Response{ - body: - "response=3&responsetext=Invalid Credit Card Number REFID:3502947912&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=sale&response_code=300", - headers: [ - {"Date", "Thu, 21 Dec 2017 13:20:08 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "155"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} + {:ok, + %HTTPoison.Response{ + body: + "response=3&responsetext=Invalid Credit Card Number REFID:3502947912&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=sale&response_code=300", + headers: [ + {"Date", "Thu, 21 Dec 2017 13:20:08 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "155"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end def with_invalid_currency do - {:ok, %HTTPoison.Response{ - body: - "response=3&responsetext=The cc payment type [Visa] and/or currency [INR] is not accepted REFID:3503238709&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=auth&response_code=300", - headers: [ - {"Date", "Tue, 26 Dec 2017 10:37:42 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "193"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} + {:ok, + %HTTPoison.Response{ + body: + "response=3&responsetext=The cc payment type [Visa] and/or currency [INR] is not accepted REFID:3503238709&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=auth&response_code=300", + headers: [ + {"Date", "Tue, 26 Dec 2017 10:37:42 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "193"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end def successful_capture do - {:ok, %HTTPoison.Response{ - body: - "response=1&responsetext=SUCCESS&authcode=123456&transactionid=3921111362&avsresponse=&cvvresponse=&orderid=&type=capture&response_code=100", - headers: [ - {"Date", "Tue, 26 Dec 2017 12:16:55 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "138"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} + {:ok, + %HTTPoison.Response{ + body: + "response=1&responsetext=SUCCESS&authcode=123456&transactionid=3921111362&avsresponse=&cvvresponse=&orderid=&type=capture&response_code=100", + headers: [ + {"Date", "Tue, 26 Dec 2017 12:16:55 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "138"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end def successful_authorize do - {:ok, %HTTPoison.Response{ - body: - "response=1&responsetext=SUCCESS&authcode=123456&transactionid=3921111362&avsresponse=N&cvvresponse=N&orderid=&type=auth&response_code=100", - headers: [ - {"Date", "Tue, 26 Dec 2017 12:16:11 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "137"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} + {:ok, + %HTTPoison.Response{ + body: + "response=1&responsetext=SUCCESS&authcode=123456&transactionid=3921111362&avsresponse=N&cvvresponse=N&orderid=&type=auth&response_code=100", + headers: [ + {"Date", "Tue, 26 Dec 2017 12:16:11 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "137"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end def invalid_transaction_id do - {:ok, %HTTPoison.Response{ - body: - "response=3&responsetext=Transaction not found REFID:3503243979&authcode=&transactionid=3921118690&avsresponse=&cvvresponse=&orderid=&type=capture&response_code=300", - headers: [ - {"Date", "Tue, 26 Dec 2017 12:39:05 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "163"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} + {:ok, + %HTTPoison.Response{ + body: + "response=3&responsetext=Transaction not found REFID:3503243979&authcode=&transactionid=3921118690&avsresponse=&cvvresponse=&orderid=&type=capture&response_code=300", + headers: [ + {"Date", "Tue, 26 Dec 2017 12:39:05 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "163"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end def more_than_authorization_amount do - {:ok, %HTTPoison.Response{ - body: - "response=3&responsetext=The specified amount of 1001 exceeds the authorization amount of 1000.00 REFID:3503244462&authcode=&transactionid=3921127126&avsresponse=&cvvresponse=&orderid=&type=capture&response_code=300", - headers: [ - {"Date", "Tue, 26 Dec 2017 13:00:55 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "214"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} + {:ok, + %HTTPoison.Response{ + body: + "response=3&responsetext=The specified amount of 1001 exceeds the authorization amount of 1000.00 REFID:3503244462&authcode=&transactionid=3921127126&avsresponse=&cvvresponse=&orderid=&type=capture&response_code=300", + headers: [ + {"Date", "Tue, 26 Dec 2017 13:00:55 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "214"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end def successful_refund do - {:ok, %HTTPoison.Response{ - body: - "response=1&responsetext=SUCCESS&authcode=&transactionid=3921158933&avsresponse=&cvvresponse=&orderid=&type=refund&response_code=100", - headers: [ - {"Date", "Tue, 26 Dec 2017 14:00:08 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "131"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} + {:ok, + %HTTPoison.Response{ + body: + "response=1&responsetext=SUCCESS&authcode=&transactionid=3921158933&avsresponse=&cvvresponse=&orderid=&type=refund&response_code=100", + headers: [ + {"Date", "Tue, 26 Dec 2017 14:00:08 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "131"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end def more_than_purchase_amount do - {:ok, %HTTPoison.Response{ - body: - "response=3&responsetext=Refund amount may not exceed the transaction balance REFID:3503249728&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=refund&response_code=300", - headers: [ - {"Date", "Tue, 26 Dec 2017 14:05:31 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "183"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} + {:ok, + %HTTPoison.Response{ + body: + "response=3&responsetext=Refund amount may not exceed the transaction balance REFID:3503249728&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=refund&response_code=300", + headers: [ + {"Date", "Tue, 26 Dec 2017 14:05:31 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "183"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end def successful_void do - {:ok, %HTTPoison.Response{ - body: - "response=1&responsetext=Transaction Void Successful&authcode=123456&transactionid=3921178863&avsresponse=&cvvresponse=&orderid=&type=void&response_code=100", - headers: [ - {"Date", "Tue, 26 Dec 2017 14:26:05 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "155"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} + {:ok, + %HTTPoison.Response{ + body: + "response=1&responsetext=Transaction Void Successful&authcode=123456&transactionid=3921178863&avsresponse=&cvvresponse=&orderid=&type=void&response_code=100", + headers: [ + {"Date", "Tue, 26 Dec 2017 14:26:05 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "155"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end def failed_authorized_with_bad_card do - {:ok, %HTTPoison.Response{ - body: - "response=3&responsetext=Invalid Credit Card Number REFID:3503305883&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=auth&response_code=300", - headers: [ - {"Date", "Wed, 27 Dec 2017 09:51:45 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "155"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} + {:ok, + %HTTPoison.Response{ + body: + "response=3&responsetext=Invalid Credit Card Number REFID:3503305883&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=auth&response_code=300", + headers: [ + {"Date", "Wed, 27 Dec 2017 09:51:45 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "155"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end def multiple_capture_on_same_transaction do - {:ok, %HTTPoison.Response{ - body: - "response=3&responsetext=A capture requires that the existing transaction be an AUTH REFID:3503316182&authcode=&transactionid=3922433984&avsresponse=&cvvresponse=&orderid=&type=capture&response_code=300", - headers: [ - {"Date", "Wed, 27 Dec 2017 13:47:12 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "201"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} + {:ok, + %HTTPoison.Response{ + body: + "response=3&responsetext=A capture requires that the existing transaction be an AUTH REFID:3503316182&authcode=&transactionid=3922433984&avsresponse=&cvvresponse=&orderid=&type=capture&response_code=300", + headers: [ + {"Date", "Wed, 27 Dec 2017 13:47:12 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "201"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end def refund_the_authorised_transaction do - {:ok, %HTTPoison.Response{ - body: - "response=3&responsetext=Refund amount may not exceed the transaction balance REFID:3503316128&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=refund&response_code=300", - headers: [ - {"Date", "Wed, 27 Dec 2017 13:45:19 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "183"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} + {:ok, + %HTTPoison.Response{ + body: + "response=3&responsetext=Refund amount may not exceed the transaction balance REFID:3503316128&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=refund&response_code=300", + headers: [ + {"Date", "Wed, 27 Dec 2017 13:45:19 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "183"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end def validate_creditcard do - {:ok, %HTTPoison.Response{ - body: - "response=1&responsetext=&authcode=&transactionid=3933708264&avsresponse=&cvvresponse=&orderid=&type=verify&response_code=100", - headers: [ - {"Date", "Thu, 04 Jan 2018 11:12:20 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "124"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} + {:ok, + %HTTPoison.Response{ + body: + "response=1&responsetext=&authcode=&transactionid=3933708264&avsresponse=&cvvresponse=&orderid=&type=verify&response_code=100", + headers: [ + {"Date", "Thu, 04 Jan 2018 11:12:20 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "124"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end end diff --git a/test/mocks/global_collect_mock.exs b/test/mocks/global_collect_mock.exs index 8bb9d553..219b9551 100644 --- a/test/mocks/global_collect_mock.exs +++ b/test/mocks/global_collect_mock.exs @@ -1,179 +1,184 @@ defmodule Gringotts.Gateways.GlobalCollectMock do - def test_for_purchase_with_valid_card do {:ok, - %HTTPoison.Response{ - body: "{\n \"creationOutput\" : {\n \"additionalReference\" : \"00000012260000000074\",\n \"externalReference\" : \"000000122600000000740000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000122600000000740000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 500,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"1218\"\n }\n }\n },\n \"status\" : \"PENDING_APPROVAL\",\n \"statusOutput\" : {\n \"isCancellable\" : true,\n \"statusCategory\" : \"PENDING_MERCHANT\",\n \"statusCode\" : 600,\n \"statusCodeChangeDateTime\" : \"20180118135349\",\n \"isAuthorized\" : true,\n \"isRefundable\" : false\n }\n }\n}", - headers: [{"Date", "Thu, 18 Jan 2018 12:53:49 GMT"}, + %HTTPoison.Response{ + body: + "{\n \"creationOutput\" : {\n \"additionalReference\" : \"00000012260000000074\",\n \"externalReference\" : \"000000122600000000740000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000122600000000740000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 500,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"1218\"\n }\n }\n },\n \"status\" : \"PENDING_APPROVAL\",\n \"statusOutput\" : {\n \"isCancellable\" : true,\n \"statusCategory\" : \"PENDING_MERCHANT\",\n \"statusCode\" : 600,\n \"statusCodeChangeDateTime\" : \"20180118135349\",\n \"isAuthorized\" : true,\n \"isRefundable\" : false\n }\n }\n}", + headers: [ + {"Date", "Thu, 18 Jan 2018 12:53:49 GMT"}, {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, {"Location", "https://api-sandbox.globalcollect.com:443/v1/1226/payments/000000122600000000740000100001"}, {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, {"Transfer-Encoding", "chunked"}, {"Content-Type", "application/json"} - ], - request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", - status_code: 201 - } - } + ], + request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", + status_code: 201 + }} end def test_for_purchase_with_invalid_card do {:ok, - %HTTPoison.Response{ - body: "{\n \"errorId\" : \"363899bd-acfb-4452-bbb0-741c0df6b4b8\",\n \"errors\" : [ {\n \"code\" : \"21000120\",\n \"requestId\" : \"980825\",\n \"propertyName\" : \"cardPaymentMethodSpecificInput.card.expiryDate\",\n \"message\" : \"cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT\",\n \"httpStatusCode\" : 400\n } ],\n \"paymentResult\" : {\n \"creationOutput\" : {\n \"additionalReference\" : \"00000012260000000075\",\n \"externalReference\" : \"000000122600000000750000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000122600000000750000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 500,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1\n }\n },\n \"status\" : \"REJECTED\",\n \"statusOutput\" : {\n \"errors\" : [ {\n \"code\" : \"21000120\",\n \"requestId\" : \"546247\",\n \"propertyName\" : \"cardPaymentMethodSpecificInput.card.expiryDate\",\n \"message\" : \"cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT\",\n \"httpStatusCode\" : 400\n } ],\n \"isCancellable\" : false,\n \"statusCategory\" : \"UNSUCCESSFUL\",\n \"statusCode\" : 100,\n \"statusCodeChangeDateTime\" : \"20180118135651\",\n \"isAuthorized\" : false,\n \"isRefundable\" : false\n }\n }\n }\n}", - headers: [ - {"Date", "Thu, 18 Jan 2018 12:56:51 GMT"}, - {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, - {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, - {"Connection", "close"}, - {"Transfer-Encoding", "chunked"}, - {"Content-Type", "application/json"} - ], - request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", - status_code: 400 - } - } + %HTTPoison.Response{ + body: + "{\n \"errorId\" : \"363899bd-acfb-4452-bbb0-741c0df6b4b8\",\n \"errors\" : [ {\n \"code\" : \"21000120\",\n \"requestId\" : \"980825\",\n \"propertyName\" : \"cardPaymentMethodSpecificInput.card.expiryDate\",\n \"message\" : \"cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT\",\n \"httpStatusCode\" : 400\n } ],\n \"paymentResult\" : {\n \"creationOutput\" : {\n \"additionalReference\" : \"00000012260000000075\",\n \"externalReference\" : \"000000122600000000750000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000122600000000750000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 500,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1\n }\n },\n \"status\" : \"REJECTED\",\n \"statusOutput\" : {\n \"errors\" : [ {\n \"code\" : \"21000120\",\n \"requestId\" : \"546247\",\n \"propertyName\" : \"cardPaymentMethodSpecificInput.card.expiryDate\",\n \"message\" : \"cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT\",\n \"httpStatusCode\" : 400\n } ],\n \"isCancellable\" : false,\n \"statusCategory\" : \"UNSUCCESSFUL\",\n \"statusCode\" : 100,\n \"statusCodeChangeDateTime\" : \"20180118135651\",\n \"isAuthorized\" : false,\n \"isRefundable\" : false\n }\n }\n }\n}", + headers: [ + {"Date", "Thu, 18 Jan 2018 12:56:51 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Connection", "close"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", + status_code: 400 + }} end def test_for_purchase_with_invalid_amount do {:ok, %HTTPoison.Response{ - body: "{\n \"errorId\" : \"8c34dc0b-776c-44e3-8cd4-b36222960153\",\n \"errors\" : [ {\n \"code\" : \"1099\",\n \"id\" : \"INVALID_VALUE\",\n \"category\" : \"CONNECT_PLATFORM_ERROR\",\n \"message\" : \"INVALID_VALUE: '50.3' is not a valid value for field 'amount'\",\n \"httpStatusCode\" : 400\n } ]\n}", - headers: [ - {"Date", "Wed, 24 Jan 2018 07:16:06 GMT"}, - {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, - {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, - {"Connection", "close"}, - {"Transfer-Encoding", "chunked"}, - {"Content-Type", "application/json"}], - request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", - status_code: 400 - } - } + body: + "{\n \"errorId\" : \"8c34dc0b-776c-44e3-8cd4-b36222960153\",\n \"errors\" : [ {\n \"code\" : \"1099\",\n \"id\" : \"INVALID_VALUE\",\n \"category\" : \"CONNECT_PLATFORM_ERROR\",\n \"message\" : \"INVALID_VALUE: '50.3' is not a valid value for field 'amount'\",\n \"httpStatusCode\" : 400\n } ]\n}", + headers: [ + {"Date", "Wed, 24 Jan 2018 07:16:06 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Connection", "close"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", + status_code: 400 + }} end def test_for_authorize_with_valid_card do {:ok, - %HTTPoison.Response{ - body: "{\n \"creationOutput\" : {\n \"additionalReference\" : \"00000012260000000065\",\n \"externalReference\" : \"000000122600000000650000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000122600000000650000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 500,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"1218\"\n }\n }\n },\n \"status\" : \"PENDING_APPROVAL\",\n \"statusOutput\" : {\n \"isCancellable\" : true,\n \"statusCategory\" : \"PENDING_MERCHANT\",\n \"statusCode\" : 600,\n \"statusCodeChangeDateTime\" : \"20180118110419\",\n \"isAuthorized\" : true,\n \"isRefundable\" : false\n }\n }\n}", - headers: [ - {"Date", "Thu, 18 Jan 2018 10:04:19 GMT"}, - {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, - {"Location", + %HTTPoison.Response{ + body: + "{\n \"creationOutput\" : {\n \"additionalReference\" : \"00000012260000000065\",\n \"externalReference\" : \"000000122600000000650000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000122600000000650000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 500,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"1218\"\n }\n }\n },\n \"status\" : \"PENDING_APPROVAL\",\n \"statusOutput\" : {\n \"isCancellable\" : true,\n \"statusCategory\" : \"PENDING_MERCHANT\",\n \"statusCode\" : 600,\n \"statusCodeChangeDateTime\" : \"20180118110419\",\n \"isAuthorized\" : true,\n \"isRefundable\" : false\n }\n }\n}", + headers: [ + {"Date", "Thu, 18 Jan 2018 10:04:19 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"Location", "https://api-sandbox.globalcollect.com:443/v1/1226/payments/000000122600000000650000100001"}, - {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, - {"Transfer-Encoding", "chunked"}, - {"Content-Type", "application/json"} - ], - request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", - status_code: 201 - } - } + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", + status_code: 201 + }} end def test_for_authorize_with_invalid_card do {:ok, - %HTTPoison.Response{ - body: "{\n \"errorId\" : \"dcdf5c8d-e475-4fbc-ac57-76123c1640a2\",\n \"errors\" : [ {\n \"code\" : \"21000120\",\n \"requestId\" : \"978754\",\n \"propertyName\" : \"cardPaymentMethodSpecificInput.card.expiryDate\",\n \"message\" : \"cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT\",\n \"httpStatusCode\" : 400\n } ],\n \"paymentResult\" : {\n \"creationOutput\" : {\n \"additionalReference\" : \"00000012260000000066\",\n \"externalReference\" : \"000000122600000000660000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000122600000000660000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 500,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1\n }\n },\n \"status\" : \"REJECTED\",\n \"statusOutput\" : {\n \"errors\" : [ {\n \"code\" : \"21000120\",\n \"requestId\" : \"978755\",\n \"propertyName\" : \"cardPaymentMethodSpecificInput.card.expiryDate\",\n \"message\" : \"cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT\",\n \"httpStatusCode\" : 400\n } ],\n \"isCancellable\" : false,\n \"statusCategory\" : \"UNSUCCESSFUL\",\n \"statusCode\" : 100,\n \"statusCodeChangeDateTime\" : \"20180118111508\",\n \"isAuthorized\" : false,\n \"isRefundable\" : false\n }\n }\n }\n}", - headers: [ - {"Date", "Thu, 18 Jan 2018 10:15:08 GMT"}, - {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, - {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, - {"Connection", "close"}, - {"Transfer-Encoding", "chunked"}, - {"Content-Type", "application/json"} - ], - request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", - status_code: 400 - } - } + %HTTPoison.Response{ + body: + "{\n \"errorId\" : \"dcdf5c8d-e475-4fbc-ac57-76123c1640a2\",\n \"errors\" : [ {\n \"code\" : \"21000120\",\n \"requestId\" : \"978754\",\n \"propertyName\" : \"cardPaymentMethodSpecificInput.card.expiryDate\",\n \"message\" : \"cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT\",\n \"httpStatusCode\" : 400\n } ],\n \"paymentResult\" : {\n \"creationOutput\" : {\n \"additionalReference\" : \"00000012260000000066\",\n \"externalReference\" : \"000000122600000000660000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000122600000000660000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 500,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1\n }\n },\n \"status\" : \"REJECTED\",\n \"statusOutput\" : {\n \"errors\" : [ {\n \"code\" : \"21000120\",\n \"requestId\" : \"978755\",\n \"propertyName\" : \"cardPaymentMethodSpecificInput.card.expiryDate\",\n \"message\" : \"cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT\",\n \"httpStatusCode\" : 400\n } ],\n \"isCancellable\" : false,\n \"statusCategory\" : \"UNSUCCESSFUL\",\n \"statusCode\" : 100,\n \"statusCodeChangeDateTime\" : \"20180118111508\",\n \"isAuthorized\" : false,\n \"isRefundable\" : false\n }\n }\n }\n}", + headers: [ + {"Date", "Thu, 18 Jan 2018 10:15:08 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Connection", "close"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", + status_code: 400 + }} end def test_for_authorize_with_invalid_amount do {:ok, - %HTTPoison.Response{body: "{\n \"errorId\" : \"1dbef568-ed86-4c8d-a3c3-74ced258d5a2\",\n \"errors\" : [ {\n \"code\" : \"1099\",\n \"id\" : \"INVALID_VALUE\",\n \"category\" : \"CONNECT_PLATFORM_ERROR\",\n \"message\" : \"INVALID_VALUE: '50.3' is not a valid value for field 'amount'\",\n \"httpStatusCode\" : 400\n } ]\n}", - headers: [ - {"Date", "Tue, 23 Jan 2018 11:18:11 GMT"}, - {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, - {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, - {"Connection", "close"}, - {"Transfer-Encoding", "chunked"}, - {"Content-Type", "application/json"} - ], - request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", - status_code: 400 - } - } + %HTTPoison.Response{ + body: + "{\n \"errorId\" : \"1dbef568-ed86-4c8d-a3c3-74ced258d5a2\",\n \"errors\" : [ {\n \"code\" : \"1099\",\n \"id\" : \"INVALID_VALUE\",\n \"category\" : \"CONNECT_PLATFORM_ERROR\",\n \"message\" : \"INVALID_VALUE: '50.3' is not a valid value for field 'amount'\",\n \"httpStatusCode\" : 400\n } ]\n}", + headers: [ + {"Date", "Tue, 23 Jan 2018 11:18:11 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Connection", "close"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", + status_code: 400 + }} end def test_for_refund do {:ok, %HTTPoison.Response{ - body: "{\n \"errorId\" : \"b6ba00d2-8f11-4822-8f32-c6d0a4d8793b\",\n \"errors\" : [ {\n \"code\" : \"300450\",\n \"message\" : \"ORDER WITHOUT REFUNDABLE PAYMENTS\",\n \"httpStatusCode\" : 400\n } ]\n}", - headers: [ - {"Date", "Wed, 24 Jan 2018 05:33:56 GMT"}, - {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, - {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, - {"Connection", "close"}, - {"Transfer-Encoding", "chunked"}, - {"Content-Type", "application/json"} - ], - request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments/000000122600000000870000100001/refund", - status_code: 400 - } - } + body: + "{\n \"errorId\" : \"b6ba00d2-8f11-4822-8f32-c6d0a4d8793b\",\n \"errors\" : [ {\n \"code\" : \"300450\",\n \"message\" : \"ORDER WITHOUT REFUNDABLE PAYMENTS\",\n \"httpStatusCode\" : 400\n } ]\n}", + headers: [ + {"Date", "Wed, 24 Jan 2018 05:33:56 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Connection", "close"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: + "https://api-sandbox.globalcollect.com/v1/1226/payments/000000122600000000870000100001/refund", + status_code: 400 + }} end def test_for_capture_with_valid_paymentid do {:ok, %HTTPoison.Response{ - body: "{\n \"payment\" : {\n \"id\" : \"000000122600000000650000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 50,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"1218\"\n }\n }\n },\n \"status\" : \"CAPTURE_REQUESTED\",\n \"statusOutput\" : {\n \"isCancellable\" : true,\n \"statusCategory\" : \"PENDING_CONNECT_OR_3RD_PARTY\",\n \"statusCode\" : 800,\n \"statusCodeChangeDateTime\" : \"20180123140826\",\n \"isAuthorized\" : true,\n \"isRefundable\" : false\n }\n }\n}", - headers: [ - {"Date", "Tue, 23 Jan 2018 13:08:26 GMT"}, - {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, - {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, - {"Transfer-Encoding", "chunked"}, - {"Content-Type", "application/json"} - ], - request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments/000000122600000000650000100001/approve", - status_code: 200 - } - } + body: + "{\n \"payment\" : {\n \"id\" : \"000000122600000000650000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 50,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"1218\"\n }\n }\n },\n \"status\" : \"CAPTURE_REQUESTED\",\n \"statusOutput\" : {\n \"isCancellable\" : true,\n \"statusCategory\" : \"PENDING_CONNECT_OR_3RD_PARTY\",\n \"statusCode\" : 800,\n \"statusCodeChangeDateTime\" : \"20180123140826\",\n \"isAuthorized\" : true,\n \"isRefundable\" : false\n }\n }\n}", + headers: [ + {"Date", "Tue, 23 Jan 2018 13:08:26 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: + "https://api-sandbox.globalcollect.com/v1/1226/payments/000000122600000000650000100001/approve", + status_code: 200 + }} end def test_for_capture_with_invalid_paymentid do {:ok, %HTTPoison.Response{ - body: "{\n \"errorId\" : \"ccb99804-0240-45b6-bb28-52aaae59d71b\",\n \"errors\" : [ {\n \"code\" : \"1002\",\n \"id\" : \"UNKNOWN_PAYMENT_ID\",\n \"category\" : \"CONNECT_PLATFORM_ERROR\",\n \"propertyName\" : \"paymentId\",\n \"message\" : \"UNKNOWN_PAYMENT_ID\",\n \"httpStatusCode\" : 404\n } ]\n}", - headers: [ - {"Date", "Tue, 23 Jan 2018 12:25:59 GMT"}, - {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, - {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, - {"Transfer-Encoding", "chunked"}, - {"Content-Type", "application/json"} - ], - request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments/30/approve", - status_code: 404 - } - } + body: + "{\n \"errorId\" : \"ccb99804-0240-45b6-bb28-52aaae59d71b\",\n \"errors\" : [ {\n \"code\" : \"1002\",\n \"id\" : \"UNKNOWN_PAYMENT_ID\",\n \"category\" : \"CONNECT_PLATFORM_ERROR\",\n \"propertyName\" : \"paymentId\",\n \"message\" : \"UNKNOWN_PAYMENT_ID\",\n \"httpStatusCode\" : 404\n } ]\n}", + headers: [ + {"Date", "Tue, 23 Jan 2018 12:25:59 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments/30/approve", + status_code: 404 + }} end def test_for_void_with_valid_card do {:ok, %HTTPoison.Response{ - body: "{\n \"payment\" : {\n \"id\" : \"000000122600000000870000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 50,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"1218\"\n }\n }\n },\n \"status\" : \"CANCELLED\",\n \"statusOutput\" : {\n \"isCancellable\" : false,\n \"statusCategory\" : \"UNSUCCESSFUL\",\n \"statusCode\" : 99999,\n \"statusCodeChangeDateTime\" : \"20180124064204\",\n \"isAuthorized\" : false,\n \"isRefundable\" : false\n }\n }\n}", - headers: [ - {"Date", "Wed, 24 Jan 2018 05:42:04 GMT"}, - {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, - {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, - {"Transfer-Encoding", "chunked"}, - {"Content-Type", "application/json"} - ], - request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments/000000122600000000870000100001/cancel", - status_code: 200 - } - } + body: + "{\n \"payment\" : {\n \"id\" : \"000000122600000000870000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 50,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"1218\"\n }\n }\n },\n \"status\" : \"CANCELLED\",\n \"statusOutput\" : {\n \"isCancellable\" : false,\n \"statusCategory\" : \"UNSUCCESSFUL\",\n \"statusCode\" : 99999,\n \"statusCodeChangeDateTime\" : \"20180124064204\",\n \"isAuthorized\" : false,\n \"isRefundable\" : false\n }\n }\n}", + headers: [ + {"Date", "Wed, 24 Jan 2018 05:42:04 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: + "https://api-sandbox.globalcollect.com/v1/1226/payments/000000122600000000870000100001/cancel", + status_code: 200 + }} end def test_for_network_failure do diff --git a/test/mocks/trexle_mock.exs b/test/mocks/trexle_mock.exs index 27c4d1c2..73f5aa9e 100644 --- a/test/mocks/trexle_mock.exs +++ b/test/mocks/trexle_mock.exs @@ -1,197 +1,213 @@ defmodule Gringotts.Gateways.TrexleMock do def test_for_purchase_with_valid_card do - {:ok, %HTTPoison.Response{ - body: ~s/{"response":{"token":"charge_3e89c6f073606ac1efe62e76e22dd7885441dc72","success":true,"captured":false}}/, - headers: [ - {"Date", "Fri, 22 Dec 2017 11:57:28 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "max-age=0, private, must-revalidate"}, - {"X-Request-Id", "9b2a1d30-9bca-48f2-862e-4090766689cb"}, - {"X-Runtime", "0.777520"}, - {"Content-Length", "104"}, - {"X-Powered-By", "PleskLin"} - ], - request_url: "https://core.trexle.com/api/v1//charges", - status_code: 201 - }} + {:ok, + %HTTPoison.Response{ + body: + ~s/{"response":{"token":"charge_3e89c6f073606ac1efe62e76e22dd7885441dc72","success":true,"captured":false}}/, + headers: [ + {"Date", "Fri, 22 Dec 2017 11:57:28 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "max-age=0, private, must-revalidate"}, + {"X-Request-Id", "9b2a1d30-9bca-48f2-862e-4090766689cb"}, + {"X-Runtime", "0.777520"}, + {"Content-Length", "104"}, + {"X-Powered-By", "PleskLin"} + ], + request_url: "https://core.trexle.com/api/v1//charges", + status_code: 201 + }} end def test_for_purchase_with_invalid_card do - {:ok, %HTTPoison.Response{ - body: ~s/{"error":"Payment failed","detail":"Your card's expiration year is invalid."}/, - headers: [ - {"Date", "Fri, 22 Dec 2017 13:20:50 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "no-cache"}, - {"X-Request-Id", "eb8100a1-8ffa-47da-9623-8d3b2af51b84"}, - {"X-Runtime", "0.445244"}, - {"Content-Length", "77"}, - {"X-Powered-By", "PleskLin"}, - {"Connection", "close"} - ], - request_url: "https://core.trexle.com/api/v1//charges", - status_code: 400 - }} + {:ok, + %HTTPoison.Response{ + body: ~s/{"error":"Payment failed","detail":"Your card's expiration year is invalid."}/, + headers: [ + {"Date", "Fri, 22 Dec 2017 13:20:50 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "no-cache"}, + {"X-Request-Id", "eb8100a1-8ffa-47da-9623-8d3b2af51b84"}, + {"X-Runtime", "0.445244"}, + {"Content-Length", "77"}, + {"X-Powered-By", "PleskLin"}, + {"Connection", "close"} + ], + request_url: "https://core.trexle.com/api/v1//charges", + status_code: 400 + }} end def test_for_purchase_with_invalid_amount do - {:ok, %HTTPoison.Response{ - body: ~s/{"error":"Payment failed","detail":"Amount must be at least 50 cents"}/, - headers: [ - {"Date", "Sat, 23 Dec 2017 18:16:33 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "no-cache"}, - {"X-Request-Id", "4ce2eea4-3ea9-4345-ac85-9bc45f22f5ac"}, - {"X-Runtime", "0.476058"}, - {"Content-Length", "70"}, - {"X-Powered-By", "PleskLin"}, - {"Connection", "close"} - ], - request_url: "https://core.trexle.com/api/v1//charges", - status_code: 400 - }} + {:ok, + %HTTPoison.Response{ + body: ~s/{"error":"Payment failed","detail":"Amount must be at least 50 cents"}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 18:16:33 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "no-cache"}, + {"X-Request-Id", "4ce2eea4-3ea9-4345-ac85-9bc45f22f5ac"}, + {"X-Runtime", "0.476058"}, + {"Content-Length", "70"}, + {"X-Powered-By", "PleskLin"}, + {"Connection", "close"} + ], + request_url: "https://core.trexle.com/api/v1//charges", + status_code: 400 + }} end def test_for_authorize_with_valid_card do - {:ok, %HTTPoison.Response{ - body: ~s/{"response":{"token":"charge_8ab2b21a2f02495f5c36b34d129c8a0e836add32","success":true,"captured":false}}/, - headers: [ - {"Date", "Sat, 23 Dec 2017 18:33:31 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "max-age=0, private, must-revalidate"}, - {"X-Request-Id", "51d28d13-81e5-41fd-b711-1b6531fdd3dd"}, - {"X-Runtime", "0.738395"}, - {"Content-Length", "104"}, - {"X-Powered-By", "PleskLin"} - ], - request_url: "https://core.trexle.com/api/v1//charges", - status_code: 201 - }} + {:ok, + %HTTPoison.Response{ + body: + ~s/{"response":{"token":"charge_8ab2b21a2f02495f5c36b34d129c8a0e836add32","success":true,"captured":false}}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 18:33:31 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "max-age=0, private, must-revalidate"}, + {"X-Request-Id", "51d28d13-81e5-41fd-b711-1b6531fdd3dd"}, + {"X-Runtime", "0.738395"}, + {"Content-Length", "104"}, + {"X-Powered-By", "PleskLin"} + ], + request_url: "https://core.trexle.com/api/v1//charges", + status_code: 201 + }} end def test_for_authorize_with_invalid_card do - {:ok, %HTTPoison.Response{ - body: ~s/{"error":"Payment failed","detail":"Your card's expiration year is invalid."}/, - headers: [ - {"Date", "Sat, 23 Dec 2017 18:25:40 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "no-cache"}, - {"X-Request-Id", "239e7054-9500-4a87-bf3b-09456d550b6d"}, - {"X-Runtime", "0.466670"}, - {"Content-Length", "77"}, - {"X-Powered-By", "PleskLin"}, - {"Connection", "close"} - ], - request_url: "https://core.trexle.com/api/v1//charges", - status_code: 400 - }} + {:ok, + %HTTPoison.Response{ + body: ~s/{"error":"Payment failed","detail":"Your card's expiration year is invalid."}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 18:25:40 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "no-cache"}, + {"X-Request-Id", "239e7054-9500-4a87-bf3b-09456d550b6d"}, + {"X-Runtime", "0.466670"}, + {"Content-Length", "77"}, + {"X-Powered-By", "PleskLin"}, + {"Connection", "close"} + ], + request_url: "https://core.trexle.com/api/v1//charges", + status_code: 400 + }} end def test_for_authorize_with_invalid_amount do - {:ok, %HTTPoison.Response{ - body: ~s/{"error":"Payment failed","detail":"Amount must be at least 50 cents"}/, - headers: [ - {"Date", "Sat, 23 Dec 2017 18:40:10 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "no-cache"}, - {"X-Request-Id", "d58db806-8016-4a0e-8519-403a969fa1a7"}, - {"X-Runtime", "0.494636"}, - {"Content-Length", "70"}, - {"X-Powered-By", "PleskLin"}, - {"Connection", "close"} - ], - request_url: "https://core.trexle.com/api/v1//charges", - status_code: 400 - }} + {:ok, + %HTTPoison.Response{ + body: ~s/{"error":"Payment failed","detail":"Amount must be at least 50 cents"}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 18:40:10 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "no-cache"}, + {"X-Request-Id", "d58db806-8016-4a0e-8519-403a969fa1a7"}, + {"X-Runtime", "0.494636"}, + {"Content-Length", "70"}, + {"X-Powered-By", "PleskLin"}, + {"Connection", "close"} + ], + request_url: "https://core.trexle.com/api/v1//charges", + status_code: 400 + }} end def test_for_refund_with_valid_token do - {:ok, %HTTPoison.Response{ - body: ~s/{"response":{"token":"refund_a86a757cc6bdabab50d6ebbfcdcd4db4e04198dd","success":true,"amount":50,"charge":"charge_cb17a0c34e870a479dfa13bd873e7ce7e090ec9b","status_message":"Transaction approved"}}/, - headers: [ - {"Date", "Sat, 23 Dec 2017 18:55:41 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "max-age=0, private, must-revalidate"}, - {"X-Request-Id", "b1c94703-7fb4-48f2-b1b4-32e3b6a87e57"}, - {"X-Runtime", "1.097186"}, - {"Content-Length", "198"}, - {"X-Powered-By", "PleskLin"} - ], - request_url: - "https://core.trexle.com/api/v1//charges/charge_cb17a0c34e870a479dfa13bd873e7ce7e090ec9b/refunds", - status_code: 201 - }} + {:ok, + %HTTPoison.Response{ + body: + ~s/{"response":{"token":"refund_a86a757cc6bdabab50d6ebbfcdcd4db4e04198dd","success":true,"amount":50,"charge":"charge_cb17a0c34e870a479dfa13bd873e7ce7e090ec9b","status_message":"Transaction approved"}}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 18:55:41 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "max-age=0, private, must-revalidate"}, + {"X-Request-Id", "b1c94703-7fb4-48f2-b1b4-32e3b6a87e57"}, + {"X-Runtime", "1.097186"}, + {"Content-Length", "198"}, + {"X-Powered-By", "PleskLin"} + ], + request_url: + "https://core.trexle.com/api/v1//charges/charge_cb17a0c34e870a479dfa13bd873e7ce7e090ec9b/refunds", + status_code: 201 + }} end def test_for_refund_with_invalid_token do - {:ok, %HTTPoison.Response{ - body: ~s/{"error":"Refund failed","detail":"invalid token"}/, - headers: [ - {"Date", "Sat, 23 Dec 2017 18:53:09 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "no-cache"}, - {"X-Request-Id", "276fd8f5-dc21-40be-8add-fa76aabbfc5b"}, - {"X-Runtime", "0.009374"}, - {"Content-Length", "50"}, - {"X-Powered-By", "PleskLin"}, - {"Connection", "close"} - ], - request_url: "https://core.trexle.com/api/v1//charges/34/refunds", - status_code: 400 - }} + {:ok, + %HTTPoison.Response{ + body: ~s/{"error":"Refund failed","detail":"invalid token"}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 18:53:09 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "no-cache"}, + {"X-Request-Id", "276fd8f5-dc21-40be-8add-fa76aabbfc5b"}, + {"X-Runtime", "0.009374"}, + {"Content-Length", "50"}, + {"X-Powered-By", "PleskLin"}, + {"Connection", "close"} + ], + request_url: "https://core.trexle.com/api/v1//charges/34/refunds", + status_code: 400 + }} end def test_for_capture_with_valid_chargetoken do - {:ok, %HTTPoison.Response{ - body: ~s/{"response":{"token":"charge_cb17a0c34e870a479dfa13bd873e7ce7e090ec9b","success":true,"captured":true,"amount":50,"status_message":"Transaction approved"}}/, - headers: [ - {"Date", "Sat, 23 Dec 2017 18:49:50 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "max-age=0, private, must-revalidate"}, - {"X-Request-Id", "97ca2db6-fd4f-4a5b-ae45-01fae9c13668"}, - {"X-Runtime", "1.092051"}, - {"Content-Length", "155"}, - {"X-Powered-By", "PleskLin"} - ], - request_url: - "https://core.trexle.com/api/v1//charges/charge_cb17a0c34e870a479dfa13bd873e7ce7e090ec9b/capture", - status_code: 200 - }} + {:ok, + %HTTPoison.Response{ + body: + ~s/{"response":{"token":"charge_cb17a0c34e870a479dfa13bd873e7ce7e090ec9b","success":true,"captured":true,"amount":50,"status_message":"Transaction approved"}}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 18:49:50 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "max-age=0, private, must-revalidate"}, + {"X-Request-Id", "97ca2db6-fd4f-4a5b-ae45-01fae9c13668"}, + {"X-Runtime", "1.092051"}, + {"Content-Length", "155"}, + {"X-Powered-By", "PleskLin"} + ], + request_url: + "https://core.trexle.com/api/v1//charges/charge_cb17a0c34e870a479dfa13bd873e7ce7e090ec9b/capture", + status_code: 200 + }} end def test_for_capture_with_invalid_chargetoken do - {:ok, %HTTPoison.Response{ - body: ~s/{"error":"Capture failed","detail":"invalid token"}/, - headers: [ - {"Date", "Sat, 23 Dec 2017 18:47:18 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "no-cache"}, - {"X-Request-Id", "b46ecb8d-7df8-4c5f-b87f-c53fae364e79"}, - {"X-Runtime", "0.010255"}, - {"Content-Length", "51"}, - {"X-Powered-By", "PleskLin"}, - {"Connection", "close"} - ], - request_url: "https://core.trexle.com/api/v1//charges/30/capture", - status_code: 400 - }} + {:ok, + %HTTPoison.Response{ + body: ~s/{"error":"Capture failed","detail":"invalid token"}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 18:47:18 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "no-cache"}, + {"X-Request-Id", "b46ecb8d-7df8-4c5f-b87f-c53fae364e79"}, + {"X-Runtime", "0.010255"}, + {"Content-Length", "51"}, + {"X-Powered-By", "PleskLin"}, + {"Connection", "close"} + ], + request_url: "https://core.trexle.com/api/v1//charges/30/capture", + status_code: 400 + }} end def test_for_store_with_valid_card do - {:ok, %HTTPoison.Response{ - body: ~s/{"response":{"token":"token_94e333959850270460e89a86bad2246613528cbb","card":{"token":"token_2a1ba29522e4a377fafa62e8e204f76ad8ba8f1e","scheme":"master","display_number":"XXXX-XXXX-XXXX-8210","expiry_year":2018,"expiry_month":1,"cvc":123,"name":"John Doe","address_line1":"456 My Street","address_line2":null,"address_city":"Ottawa","address_state":"ON","address_postcode":"K1C2N6","address_country":"CA","primary":true}}}/, - headers: [ - {"Date", "Sat, 23 Dec 2017 19:32:58 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "max-age=0, private, must-revalidate"}, - {"X-Request-Id", "1a334b22-8e01-4e1b-8b58-90dfd0b7c12f"}, - {"X-Runtime", "0.122441"}, - {"Content-Length", "422"}, - {"X-Powered-By", "PleskLin"} - ], - request_url: "https://core.trexle.com/api/v1//customers", - status_code: 201 - }} + {:ok, + %HTTPoison.Response{ + body: + ~s/{"response":{"token":"token_94e333959850270460e89a86bad2246613528cbb","card":{"token":"token_2a1ba29522e4a377fafa62e8e204f76ad8ba8f1e","scheme":"master","display_number":"XXXX-XXXX-XXXX-8210","expiry_year":2018,"expiry_month":1,"cvc":123,"name":"John Doe","address_line1":"456 My Street","address_line2":null,"address_city":"Ottawa","address_state":"ON","address_postcode":"K1C2N6","address_country":"CA","primary":true}}}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 19:32:58 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "max-age=0, private, must-revalidate"}, + {"X-Request-Id", "1a334b22-8e01-4e1b-8b58-90dfd0b7c12f"}, + {"X-Runtime", "0.122441"}, + {"Content-Length", "422"}, + {"X-Powered-By", "PleskLin"} + ], + request_url: "https://core.trexle.com/api/v1//customers", + status_code: 201 + }} end def test_for_network_failure do From c28bd8511a17feda3419113da9a585aeacd45665 Mon Sep 17 00:00:00 2001 From: Jyoti Gautam Date: Fri, 30 Mar 2018 12:05:03 +0530 Subject: [PATCH 15/24] [global-collect] Layout, docs improvements and code refactors (#111) [global-collect] Layout, docs and code refactor =============================================== New features ------------ Risk, AVS, CVS fields added in `Response` struct! Layout, docs ----------- * `credo` issues resolved. * Corrected `amount` in examples * Ran the elixir 1.6 code formatter - Used sigils in mocks Code refactors -------------- * Removed unnecessary functions - Reduced arity of `add_money` * Refactored Timex usage * Removed a test on `validate_config` as it is already tested. --- lib/gringotts/gateways/global_collect.ex | 411 ++++++++++++----------- test/gateways/global_collect_test.exs | 30 +- test/mocks/global_collect_mock.exs | 88 ++++- 3 files changed, 294 insertions(+), 235 deletions(-) diff --git a/lib/gringotts/gateways/global_collect.ex b/lib/gringotts/gateways/global_collect.ex index 624a8950..6075451d 100644 --- a/lib/gringotts/gateways/global_collect.ex +++ b/lib/gringotts/gateways/global_collect.ex @@ -2,9 +2,9 @@ defmodule Gringotts.Gateways.GlobalCollect do @moduledoc """ [GlobalCollect][home] gateway implementation. - For further details, please refer [GlobalCollect API documentation](https://epayments-api.developer-ingenico.com/s2sapi/v1/en_US/index.html). + For further details, please refer [GlobalCollect API documentation][docs]. - Following are the features that have been implemented for the GlobalCollect Gateway: + Following are the features that have been implemented for GlobalCollect: | Action | Method | | ------ | ------ | @@ -14,32 +14,34 @@ defmodule Gringotts.Gateways.GlobalCollect do | Refund | `refund/3` | | Void | `void/2` | - ## Optional or extra parameters + ## Optional parameters Most `Gringotts` API calls accept an optional `Keyword` list `opts` to supply optional arguments for transactions with the gateway. - | Key | Status | + | Key | Remark | | ---- | --- | - | `merchantCustomerId` | implemented | - | `description` | implemented | - | `customer_name` | implemented | - | `dob` | implemented | - | `company` | implemented | - | `email` | implemented | - | `phone` | implemented | - | `order_id` | implemented | - | `invoice` | implemented | - | `billingAddress` | implemented | - | `shippingAddress` | implemented | - | `name` | implemented | - | `skipAuthentication` | implemented | - + | `merchantCustomerId` | Identifier for the consumer that can be used as a search criteria in the Global Collect Payment Console | + | `description` | Descriptive text that is used towards to consumer, either during an online checkout at a third party and/or on the statement of the consumer | + | `dob` | The date of birth of the consumer Format: YYYYMMDD | + | `company` | Name of company, as a consumer | + | `email` | Email address of the consumer | + | `phone` | Phone number of the consumer | + | `invoice` | Object containing additional invoice data | + | `billingAddress` | Object containing billing address details | + | `shippingAddress` | Object containing shipping address details | + | `name` | Object containing the name details of the consumer | + | `skipAuthentication` | 3D Secure Authentication will be skipped for this transaction if set to true | + + For more details of the required keys refer [this.][options] ## Registering your GlobalCollect account at `Gringotts` - After creating your account successfully on [GlobalCollect](http://www.globalcollect.com/) follow the [dashboard link](https://sandbox.account.ingenico.com/#/account/apikey) to fetch the secret_api_key, api_key_id and [here](https://sandbox.account.ingenico.com/#/account/merchantid) for merchant_id. + After creating your account successfully on [GlobalCollect][home] open the + [dashboard][dashboard] to fetch the `secret_api_key`, `api_key_id` and + `merchant_id` from the menu. - Here's how the secrets map to the required configuration parameters for GlobalCollect: + Here's how the secrets map to the required configuration parameters for + GlobalCollect: | Config parameter | GlobalCollect secret | | ------- | ---- | @@ -47,19 +49,22 @@ defmodule Gringotts.Gateways.GlobalCollect do | `:api_key_id` | **ApiKeyId** | | `:merchant_id` | **MerchantId** | - Your Application config **must include the `[:secret_api_key, :api_key_id, :merchant_id]` field(s)** and would look - something like this: + Your Application config **must include the `:secret_api_key`, `:api_key_id`, + `:merchant_id` field(s)** and would look something like this: config :gringotts, Gringotts.Gateways.GlobalCollect, secret_api_key: "your_secret_secret_api_key" api_key_id: "your_secret_api_key_id" merchant_id: "your_secret_merchant_id" + ## Scope of this module + + * [All amount fields in globalCollect are in cents with each amount having 2 decimals.][amountReference] + ## Supported currencies and countries - The GlobalCollect platform is able to support payments in [over 150 currencies][currencies] + The GlobalCollect platform supports payments in [over 150 currencies][currencies]. - [currencies]: https://epayments.developer-ingenico.com/best-practices/services/currency-conversion ## Following the examples 1. First, set up a sample application and configure it to work with GlobalCollect. @@ -67,51 +72,74 @@ defmodule Gringotts.Gateways.GlobalCollect do - 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-globalcollect-account-at-GlobalCollect). + as described [above](#module-registering-your-globalcollect-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): + aliases to it (to save some time): ``` iex> alias Gringotts.{Response, CreditCard, Gateways.GlobalCollect} - iex> shippingAddress = %{ - street: "Desertroad", - houseNumber: "1", - additionalInfo: "Suite II", - zip: "84536", - city: "Monument Valley", - state: "Utah", - countryCode: "US" - } + street: "Desertroad", + houseNumber: "1", + additionalInfo: "Suite II", + zip: "84536", + city: "Monument Valley", + state: "Utah", + countryCode: "US" + } iex> billingAddress = %{ - street: "Desertroad", - houseNumber: "13", - additionalInfo: "b", - zip: "84536", - city: "Monument Valley", - state: "Utah", - countryCode: "US" - } + street: "Desertroad", + houseNumber: "13", + additionalInfo: "b", + zip: "84536", + city: "Monument Valley", + state: "Utah", + countryCode: "US" + } iex> invoice = %{ - invoiceNumber: "000000123", - invoiceDate: "20140306191500" - } + invoiceNumber: "000000123", + invoiceDate: "20140306191500" + } iex> name = %{ - title: "Miss", - firstName: "Road", - surname: "Runner" - } + title: "Miss", + firstName: "Road", + surname: "Runner" + } - iex> opts = [ description: "Store Purchase 1437598192", merchantCustomerId: "234", customer_name: "John Doe", dob: "19490917", company: "asma", email: "johndoe@gmail.com", phone: "7765746563", order_id: "2323", invoice: invoice, billingAddress: billingAddress, shippingAddress: shippingAddress, name: name, skipAuthentication: "true" ] + iex> card = %CreditCard{ + number: "4567350000427977", + month: 12, + year: 43, + first_name: "John", + last_name: "Doe", + verification_code: "123", + brand: "VISA" + } + iex> opts = [ + description: "Store Purchase 1437598192", + merchantCustomerId: "234", dob: "19490917", + company: "asma", email: "johndoe@gmail.com", + phone: "7765746563", invoice: invoice, + billingAddress: billingAddress, + shippingAddress: shippingAddress, + name: name, skipAuthentication: "true" + ] ``` We'll be using these in the examples below. + [home]: http://www.globalcollect.com/ + [docs]: https://epayments-api.developer-ingenico.com/s2sapi/v1/en_US/index.html + [dashboard]: https://sandbox.account.ingenico.com/#/dashboard + [gs]: # + [options]: https://epayments-api.developer-ingenico.com/s2sapi/v1/en_US/java/payments/create.html#payments-create-payload + [currencies]: https://epayments.developer-ingenico.com/best-practices/services/currency-conversion [example]: https://github.com/aviabird/gringotts_example + [amountReference]: https://epayments-api.developer-ingenico.com/c2sapi/v1/en_US/swift/services/convertAmount.html """ @base_url "https://api-sandbox.globalcollect.com/v1/" @@ -127,12 +155,12 @@ defmodule Gringotts.Gateways.GlobalCollect do alias Gringotts.{Money, CreditCard, Response} @brand_map %{ - visa: "1", - american_express: "2", - master: "3", - discover: "128", - jcb: "125", - diners_club: "132" + VISA: "1", + AMERICAN_EXPRESS: "2", + MASTER: "3", + DISCOVER: "128", + JCB: "125", + DINERS_CLUB: "132" } @doc """ @@ -143,73 +171,70 @@ defmodule Gringotts.Gateways.GlobalCollect do also triggers risk management. Funds are not transferred. GlobalCollect returns a payment id which can be further used to: - * `capture/3` _an_ amount. - * `refund/3` _an_amount + * `capture/3` an amount. + * `refund/3` an amount * `void/2` a pre_authorization ## Example - > The following session shows how one would (pre) authorize a payment of $100 on + The following example shows how one would (pre) authorize a payment of $100 on a sample `card`. ``` iex> card = %CreditCard{ number: "4567350000427977", month: 12, - year: 18, + year: 43, first_name: "John", last_name: "Doe", verification_code: "123", - brand: "visa" + brand: "VISA" } - iex> amount = %{value: Decimal.new(100), currency: "USD"} + iex> amount = Money.new(100, :USD) iex> {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.GlobalCollect, amount, card, opts) ``` """ @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} - def authorize(amount, card = %CreditCard{}, opts) do - params = create_params_for_auth_or_purchase(amount, card, opts) + def authorize(amount, %CreditCard{} = card, opts) do + params = %{ + order: add_order(amount, opts), + cardPaymentMethodSpecificInput: add_card(card, opts) + } + commit(:post, "payments", params, opts) end @doc """ Captures a pre-authorized `amount`. - `amount` is transferred to the merchant account by GlobalCollect used in the - pre-authorization referenced by `payment_id`. + `amount` used in the pre-authorization referenced by `payment_id` is + transferred to the merchant account by GlobalCollect. ## Note - > Authorized payment with PENDING_APPROVAL status only allow a single capture whereas the one with PENDING_CAPTURE status is used for payments that allow multiple captures. - > PENDING_APPROVAL is a common status only with card and direct debit transactions. + Authorized payment with PENDING_APPROVAL status only allow a single capture whereas + the one with PENDING_CAPTURE status is used for payments that allow multiple captures. ## Example - The following session shows how one would (partially) capture a previously + The following example shows how one would (partially) capture a previously authorized a payment worth $100 by referencing the obtained authorization `id`. ``` - iex> card = %CreditCard{ - number: "4567350000427977", - month: 12, - year: 18, - first_name: "John", - last_name: "Doe", - verification_code: "123", - brand: "visa" - } - - iex> amount = %{value: Decimal.new(100), currency: "USD"} + iex> amount = Money.new(100, :USD) - iex> {:ok, capture_result} = Gringotts.capture(Gringotts.Gateways.GlobalCollect, amount, card, opts) + iex> {:ok, capture_result} = Gringotts.capture(Gringotts.Gateways.GlobalCollect, auth_result.authorization, amount, opts) ``` """ @spec capture(String.t(), Money.t(), keyword) :: {:ok | :error, Response} def capture(payment_id, amount, opts) do - params = create_params_for_capture(amount, opts) + params = %{ + order: add_order(amount, opts) + } + commit(:post, "payments/#{payment_id}/approve", params, opts) end @@ -222,28 +247,28 @@ defmodule Gringotts.Gateways.GlobalCollect do ## Example - > The following session shows how one would process a payment in one-shot, + The following example shows how one would process a payment in one-shot, without (pre) authorization. ``` iex> card = %CreditCard{ number: "4567350000427977", month: 12, - year: 18, + year: 43, first_name: "John", last_name: "Doe", verification_code: "123", - brand: "visa" + brand: "VISA" } - iex> amount = %{value: Decimal.new(100), currency: "USD"} + iex> amount = Money.new(100, :USD) iex> {:ok, purchase_result} = Gringotts.purchase(Gringotts.Gateways.GlobalCollect, amount, card, opts) ``` """ @spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} - def purchase(amount, card = %CreditCard{}, opts) do + def purchase(amount, %CreditCard{} = card, opts) do case authorize(amount, card, opts) do {:ok, results} -> payment_id = results.raw["payment"]["id"] @@ -257,50 +282,53 @@ defmodule Gringotts.Gateways.GlobalCollect do @doc """ Voids the referenced payment. - This makes it impossible to process the payment any further and will also try to reverse an authorization on a card. - Reversing an authorization that you will not be utilizing will prevent you from having to pay a fee/penalty for unused authorization requests. + This makes it impossible to process the payment any further and will also try + to reverse an authorization on a card. + Reversing an authorization that you will not be utilizing will prevent you + from having to [pay a fee/penalty][void] for unused authorization requests. + [void]: https://epayments-api.developer-ingenico.com/s2sapi/v1/en_US/java/payments/cancel.html#payments-cancel-request ## Example - > The following session shows how one would void a previous (pre) + The following example shows how one would void a previous (pre) authorization. Remember that our `capture/3` example only did a complete capture. ``` - iex> {:ok, void_result} = Gringotts.void(Gringotts.Gateways.GlobalCollect, auth_result.payment.id, opts) + iex> {:ok, void_result} = Gringotts.void(Gringotts.Gateways.GlobalCollect, auth_result.authorization, opts) ``` """ @spec void(String.t(), keyword) :: {:ok | :error, Response} def void(payment_id, opts) do - params = nil - commit(:post, "payments/#{payment_id}/cancel", params, opts) + commit(:post, "payments/#{payment_id}/cancel", [], opts) end @doc """ Refunds the `amount` to the customer's account with reference to a prior transfer. - > You can refund any transaction by just calling this API - - ## Note - You always have the option to refund just a portion of the payment amount. - It is also possible to submit multiple refund requests on one payment as long as the total amount to be refunded does not exceed the total amount that was paid. + It is also possible to submit multiple refund requests on one payment as long + as the total amount to be refunded does not exceed the total amount that was paid. ## Example - > The following session shows how one would refund a previous purchase (and + The following example shows how one would refund a previous purchase (and similarily for captures). ``` - iex> amount = %{value: Decimal.new(100), currency: "USD"} + iex> amount = Money.new(100, :USD) - iex> {:ok, refund_result} = Gringotts.refund(Gringotts.Gateways.GlobalCollect, auth_result.payment.id, amount) + iex> {:ok, refund_result} = Gringotts.refund(Gringotts.Gateways.GlobalCollect, auth_result.authorization, amount) ``` """ @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response} def refund(amount, payment_id, opts) do - params = create_params_for_refund(amount, opts) + params = %{ + amountOfMoney: add_money(amount), + customer: add_customer(opts) + } + commit(:post, "payments/#{payment_id}/refund", params, opts) end @@ -308,40 +336,18 @@ defmodule Gringotts.Gateways.GlobalCollect do # PRIVATE METHODS # ############################################################################### - # Makes the request to GlobalCollect's network. - # For consistency with other gateway implementations, make your (final) - # network request in here, and parse it using another private method called - # `respond`. - - defp create_params_for_refund(amount, opts) do - %{ - amountOfMoney: add_money(amount, opts), - customer: add_customer(opts) - } - end - - defp create_params_for_auth_or_purchase(amount, payment, opts) do - %{ - order: add_order(amount, opts), - cardPaymentMethodSpecificInput: add_payment(payment, @brand_map, opts) - } - end - - defp create_params_for_capture(amount, opts) do - %{ - order: add_order(amount, opts) - } - end - defp add_order(money, options) do %{ - amountOfMoney: add_money(money, options), + amountOfMoney: add_money(money), customer: add_customer(options), - references: add_references(options) + references: %{ + descriptor: options[:description], + invoiceData: options[:invoice] + } } end - defp add_money(amount, options) do + defp add_money(amount) do {currency, amount, _} = Money.to_integer(amount) %{ @@ -353,94 +359,65 @@ defmodule Gringotts.Gateways.GlobalCollect do defp add_customer(options) do %{ merchantCustomerId: options[:merchantCustomerId], - personalInformation: personal_info(options), + personalInformation: %{ + name: options[:name] + }, dateOfBirth: options[:dob], - companyInformation: company_info(options), + companyInformation: %{ + name: options[:company] + }, billingAddress: options[:billingAddress], shippingAddress: options[:shippingAddress], - contactDetails: contact(options) - } - end - - defp add_references(options) do - %{ - descriptor: options[:description], - invoiceData: options[:invoice] - } - end - - defp personal_info(options) do - %{ - name: options[:name] - } - end - - defp company_info(options) do - %{ - name: options[:company] - } - end - - defp contact(options) do - %{ - emailAddress: options[:email], - phoneNumber: options[:phone] + contactDetails: %{ + emailAddress: options[:email], + phoneNumber: options[:phone] + } } end - def add_card(%CreditCard{} = payment) do + defp add_card(card, opts) do %{ - cvv: payment.verification_code, - cardNumber: payment.number, - expiryDate: "#{payment.month}" <> "#{payment.year}", - cardholderName: CreditCard.full_name(payment) - } - end - - defp add_payment(payment, brand_map, opts) do - brand = payment.brand - - %{ - paymentProductId: Map.fetch!(brand_map, String.to_atom(brand)), + paymentProductId: Map.fetch!(@brand_map, String.to_atom(card.brand)), skipAuthentication: opts[:skipAuthentication], - card: add_card(payment) + card: %{ + cvv: card.verification_code, + cardNumber: card.number, + expiryDate: "#{card.month}#{card.year}", + cardholderName: CreditCard.full_name(card) + } } end - defp auth_digest(path, secret_api_key, time, opts) do - data = "POST\napplication/json\n#{time}\n/v1/#{opts[:config][:merchant_id]}/#{path}\n" - :crypto.hmac(:sha256, secret_api_key, data) - end - defp commit(method, path, params, opts) do headers = create_headers(path, opts) data = Poison.encode!(params) - url = "#{@base_url}#{opts[:config][:merchant_id]}/#{path}" - response = HTTPoison.request(method, url, data, headers) - response |> respond + merchant_id = opts[:config][:merchant_id] + url = "#{@base_url}#{merchant_id}/#{path}" + + gateway_response = HTTPoison.request(method, url, data, headers) + gateway_response |> respond end defp create_headers(path, opts) do - time = date + datetime = Timex.now() |> Timex.local() + + date_string = + "#{Timex.format!(datetime, "%a, %d %b %Y %H:%M:%S", :strftime)} #{datetime.zone_abbr}" - sha_signature = - auth_digest(path, opts[:config][:secret_api_key], time, opts) |> Base.encode64() + api_key_id = opts[:config][:api_key_id] - auth_token = "GCS v1HMAC:#{opts[:config][:api_key_id]}:#{sha_signature}" + sha_signature = auth_digest(path, date_string, opts) - headers = [ - {"Content-Type", "application/json"}, - {"Authorization", auth_token}, - {"Date", time} - ] + auth_token = "GCS v1HMAC:#{api_key_id}:#{Base.encode64(sha_signature)}" + [{"Content-Type", "application/json"}, {"Authorization", auth_token}, {"Date", date_string}] end - defp date() do - use Timex - datetime = Timex.now() |> Timex.local() - strftime_str = Timex.format!(datetime, "%a, %d %b %Y %H:%M:%S ", :strftime) - time_zone = Timex.timezone(:local, datetime) - time = strftime_str <> "#{time_zone.abbreviation}" + defp auth_digest(path, date_string, opts) do + secret_api_key = opts[:config][:secret_api_key] + merchant_id = opts[:config][:merchant_id] + + data = "POST\napplication/json\n#{date_string}\n/v1/#{merchant_id}/#{path}\n" + :crypto.hmac(:sha256, secret_api_key, data) end # Parses GlobalCollect's response and returns a `Gringotts.Response` struct @@ -450,7 +427,31 @@ defmodule Gringotts.Gateways.GlobalCollect do defp respond({:ok, %{status_code: code, body: body}}) when code in [200, 201] do case decode(body) do - {:ok, results} -> {:ok, Response.success(raw: results, status_code: code)} + {:ok, results} -> + { + :ok, + Response.success( + authorization: results["payment"]["id"], + raw: results, + status_code: code, + avs_result: + results["payment"]["paymentOutput"]["cardPaymentMethodSpecificOutput"][ + "fraudResults" + ]["avsResult"], + cvc_result: + results["payment"]["paymentOutput"]["cardPaymentMethodSpecificOutput"][ + "fraudResults" + ]["cvcResult"], + message: results["payment"]["status"], + fraud_review: + results["payment"]["paymentOutput"]["cardPaymentMethodSpecificOutput"][ + "fraudResults" + ]["fraudServiceResult"] + ) + } + + {:error, _} -> + {:error, Response.error(raw: body, message: "undefined response from GlobalCollect")} end end @@ -462,11 +463,13 @@ defmodule Gringotts.Gateways.GlobalCollect do end defp respond({:error, %HTTPoison.Error{} = error}) do - {:error, - Response.error( - code: error.id, - reason: :network_fail?, - description: "HTTPoison says '#{error.reason}'" - )} + { + :error, + Response.error( + code: error.id, + reason: :network_fail?, + description: "HTTPoison says '#{error.reason}'" + ) + } end end diff --git a/test/gateways/global_collect_test.exs b/test/gateways/global_collect_test.exs index d64489c2..8d09449c 100644 --- a/test/gateways/global_collect_test.exs +++ b/test/gateways/global_collect_test.exs @@ -14,7 +14,7 @@ defmodule Gringotts.Gateways.GlobalCollectTest do @bad_amount Money.new("50.3", :USD) - @shippingAddress %{ + @shipping_address %{ street: "Desertroad", houseNumber: "1", additionalInfo: "Suite II", @@ -31,7 +31,7 @@ defmodule Gringotts.Gateways.GlobalCollectTest do first_name: "John", last_name: "Doe", verification_code: "123", - brand: "visa" + brand: "VISA" } @invalid_card %CreditCard{ @@ -41,10 +41,10 @@ defmodule Gringotts.Gateways.GlobalCollectTest do first_name: "John", last_name: "Doe", verification_code: "123", - brand: "visa" + brand: "VISA" } - @billingAddress %{ + @billing_address %{ street: "Desertroad", houseNumber: "13", additionalInfo: "b", @@ -71,16 +71,16 @@ defmodule Gringotts.Gateways.GlobalCollectTest do @invalid_config [ config: %{ - secret_api_key: "Qtg9v4Q0G13sLRNcClWhHnvN1kVYWDcy4w9rG8T86XU=", - api_key_id: "e5743abfc360ed12" + secret_api_key: "some_secret_api_key", + api_key_id: "some_api_key_id" } ] @options [ config: %{ - secret_api_key: "Qtg9v4Q0G13sLRNcClWhHnvN1kVYWDcy4w9rG8T86XU=", - api_key_id: "e5743abfc360ed12", - merchant_id: "1226" + secret_api_key: "some_secret_api_key", + api_key_id: "some_api_key_id", + merchant_id: "some_merchant_id" }, description: "Store Purchase 1437598192", merchantCustomerId: "234", @@ -91,20 +91,12 @@ defmodule Gringotts.Gateways.GlobalCollectTest do phone: "7468474533", order_id: "2323", invoice: @invoice, - billingAddress: @billingAddress, - shippingAddress: @shippingAddress, + billingAddress: @billing_address, + shippingAddress: @shipping_address, name: @name, skipAuthentication: "true" ] - describe "validation arguments check" do - test "with no merchant id passed in config" do - assert_raise ArgumentError, fn -> - GlobalCollect.validate_config(@invalid_config) - end - end - end - describe "purchase" do test "with valid card" do with_mock HTTPoison, diff --git a/test/mocks/global_collect_mock.exs b/test/mocks/global_collect_mock.exs index 219b9551..ebedf90a 100644 --- a/test/mocks/global_collect_mock.exs +++ b/test/mocks/global_collect_mock.exs @@ -3,7 +3,15 @@ defmodule Gringotts.Gateways.GlobalCollectMock do {:ok, %HTTPoison.Response{ body: - "{\n \"creationOutput\" : {\n \"additionalReference\" : \"00000012260000000074\",\n \"externalReference\" : \"000000122600000000740000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000122600000000740000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 500,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"1218\"\n }\n }\n },\n \"status\" : \"PENDING_APPROVAL\",\n \"statusOutput\" : {\n \"isCancellable\" : true,\n \"statusCategory\" : \"PENDING_MERCHANT\",\n \"statusCode\" : 600,\n \"statusCodeChangeDateTime\" : \"20180118135349\",\n \"isAuthorized\" : true,\n \"isRefundable\" : false\n }\n }\n}", + ~s/{"creationOutput":{"additionalReference":"00000012260000000074","externalReference": + "000000122600000000740000100001"},"payment":{"id":"000000122600000000740000100001", + "paymentOutput":{"amountOfMoney":{"amount":500,"currencyCode":"USD"},"references": + {"paymentReference":"0"},"paymentMethod":"card","cardPaymentMethodSpecificOutput": + {"paymentProductId":1,"authorisationCode":"OK1131","fraudResults":{"fraudServiceResult": + "no-advice","avsResult":"0","cvvResult":"0"},"card":{"cardNumber":"************7977", + "expiryDate":"1218"}}},"status":"PENDING_APPROVAL","statusOutput":{"isCancellable":true, + "statusCategory":"PENDING_MERCHANT","statusCode":600,"statusCodeChangeDateTime": + "20180118135349","isAuthorized":true,"isRefundable":false}}}/, headers: [ {"Date", "Thu, 18 Jan 2018 12:53:49 GMT"}, {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, @@ -22,7 +30,18 @@ defmodule Gringotts.Gateways.GlobalCollectMock do {:ok, %HTTPoison.Response{ body: - "{\n \"errorId\" : \"363899bd-acfb-4452-bbb0-741c0df6b4b8\",\n \"errors\" : [ {\n \"code\" : \"21000120\",\n \"requestId\" : \"980825\",\n \"propertyName\" : \"cardPaymentMethodSpecificInput.card.expiryDate\",\n \"message\" : \"cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT\",\n \"httpStatusCode\" : 400\n } ],\n \"paymentResult\" : {\n \"creationOutput\" : {\n \"additionalReference\" : \"00000012260000000075\",\n \"externalReference\" : \"000000122600000000750000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000122600000000750000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 500,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1\n }\n },\n \"status\" : \"REJECTED\",\n \"statusOutput\" : {\n \"errors\" : [ {\n \"code\" : \"21000120\",\n \"requestId\" : \"546247\",\n \"propertyName\" : \"cardPaymentMethodSpecificInput.card.expiryDate\",\n \"message\" : \"cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT\",\n \"httpStatusCode\" : 400\n } ],\n \"isCancellable\" : false,\n \"statusCategory\" : \"UNSUCCESSFUL\",\n \"statusCode\" : 100,\n \"statusCodeChangeDateTime\" : \"20180118135651\",\n \"isAuthorized\" : false,\n \"isRefundable\" : false\n }\n }\n }\n}", + ~s/{"errorId" : "363899bd-acfb-4452-bbb0-741c0df6b4b8","errors" : [ {"code" : "21000120", + "requestId" : "980825","propertyName" : "cardPaymentMethodSpecificInput.card.expiryDate", + "message" : "cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT", + "httpStatusCode" : 400} ],"paymentResult" : {"creationOutput" : { " additionalReference" : "00000012260000000075", + "externalReference" : "000000122600000000750000100001"},"payment" : {"id" : "000000122600000000750000100001", + "paymentOutput" : {"amountOfMoney" : {"amount" : 500,"currencyCode" : "USD"},"references" : {"paymentReference" : "0"}, + "paymentMethod" : "card","cardPaymentMethodSpecificOutput" : {"paymentProductId" : 1}}, + "status" : "REJECTED","statusOutput" : {"errors" : [ {"code" : "21000120", + "requestId" : "546247","propertyName" : "cardPaymentMethodSpecificInput.card.expiryDate", + "message" : "cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT", + "httpStatusCode" : 400} ],"isCancellable" : false,"statusCategory" : "UNSUCCESSFUL","statusCode" : 100, + "statusCodeChangeDateTime" : "20180118135651","isAuthorized" : false,"isRefundable" : false}}}}/, headers: [ {"Date", "Thu, 18 Jan 2018 12:56:51 GMT"}, {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, @@ -40,7 +59,10 @@ defmodule Gringotts.Gateways.GlobalCollectMock do {:ok, %HTTPoison.Response{ body: - "{\n \"errorId\" : \"8c34dc0b-776c-44e3-8cd4-b36222960153\",\n \"errors\" : [ {\n \"code\" : \"1099\",\n \"id\" : \"INVALID_VALUE\",\n \"category\" : \"CONNECT_PLATFORM_ERROR\",\n \"message\" : \"INVALID_VALUE: '50.3' is not a valid value for field 'amount'\",\n \"httpStatusCode\" : 400\n } ]\n}", + ~s/{ "errorId" : "8c34dc0b-776c-44e3-8cd4-b36222960153","errors" : [ {"code" : "1099","id" : + "INVALID_VALUE","category" : "CONNECT_PLATFORM_ERROR","message" : + "INVALID_VALUE: '50.3' is not a valid value for field 'amount'", + "httpStatusCode" : 400 } ]}/, headers: [ {"Date", "Wed, 24 Jan 2018 07:16:06 GMT"}, {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, @@ -58,7 +80,16 @@ defmodule Gringotts.Gateways.GlobalCollectMock do {:ok, %HTTPoison.Response{ body: - "{\n \"creationOutput\" : {\n \"additionalReference\" : \"00000012260000000065\",\n \"externalReference\" : \"000000122600000000650000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000122600000000650000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 500,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"1218\"\n }\n }\n },\n \"status\" : \"PENDING_APPROVAL\",\n \"statusOutput\" : {\n \"isCancellable\" : true,\n \"statusCategory\" : \"PENDING_MERCHANT\",\n \"statusCode\" : 600,\n \"statusCodeChangeDateTime\" : \"20180118110419\",\n \"isAuthorized\" : true,\n \"isRefundable\" : false\n }\n }\n}", + ~s/{"creationOutput" : {"additionalReference" : "00000012260000000065","externalReference" : + "000000122600000000650000100001"},"payment" : {"id" : "000000122600000000650000100001", + "paymentOutput" :{"amountOfMoney" : {"amount" : 500,"currencyCode" : "USD"},"references" : + {"paymentReference" : "0" },"paymentMethod" : "card","cardPaymentMethodSpecificOutput" : + {"paymentProductId" : 1,"authorisationCode" : "OK1131","fraudResults" : + {"fraudServiceResult" : "no-advice","avsResult" : "0","cvvResult" : "0"},"card" : + {"cardNumber" : "************7977","expiryDate" : "1218"}}},"status" : "PENDING_APPROVAL", + "statusOutput" : {"isCancellable" : true,"statusCategory" : "PENDING_MERCHANT","statusCode" + : 600,"statusCodeChangeDateTime" : "20180118110419","isAuthorized" : true, + "isRefundable" : false}}}/, headers: [ {"Date", "Thu, 18 Jan 2018 10:04:19 GMT"}, {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, @@ -77,7 +108,21 @@ defmodule Gringotts.Gateways.GlobalCollectMock do {:ok, %HTTPoison.Response{ body: - "{\n \"errorId\" : \"dcdf5c8d-e475-4fbc-ac57-76123c1640a2\",\n \"errors\" : [ {\n \"code\" : \"21000120\",\n \"requestId\" : \"978754\",\n \"propertyName\" : \"cardPaymentMethodSpecificInput.card.expiryDate\",\n \"message\" : \"cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT\",\n \"httpStatusCode\" : 400\n } ],\n \"paymentResult\" : {\n \"creationOutput\" : {\n \"additionalReference\" : \"00000012260000000066\",\n \"externalReference\" : \"000000122600000000660000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000122600000000660000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 500,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1\n }\n },\n \"status\" : \"REJECTED\",\n \"statusOutput\" : {\n \"errors\" : [ {\n \"code\" : \"21000120\",\n \"requestId\" : \"978755\",\n \"propertyName\" : \"cardPaymentMethodSpecificInput.card.expiryDate\",\n \"message\" : \"cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT\",\n \"httpStatusCode\" : 400\n } ],\n \"isCancellable\" : false,\n \"statusCategory\" : \"UNSUCCESSFUL\",\n \"statusCode\" : 100,\n \"statusCodeChangeDateTime\" : \"20180118111508\",\n \"isAuthorized\" : false,\n \"isRefundable\" : false\n }\n }\n }\n}", + ~s/{"errorId" : "dcdf5c8d-e475-4fbc-ac57-76123c1640a2","errors" : [ {"code" : "21000120", + "requestId" : "978754","propertyName" : "cardPaymentMethodSpecificInput.card.expiryDate","message": + "cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT", + "httpStatusCode" : 400} ],"paymentResult" : {"creationOutput" : + {"additionalReference" :"00000012260000000066","externalReference" : + "000000122600000000660000100001"},"payment" :{"id" : "000000122600000000660000100001", + "paymentOutput" : {"amountOfMoney" : {"amount" : 500,"currencyCode" : "USD"}, + "references" : {"paymentReference" : "0"},"paymentMethod" : "card", + "cardPaymentMethodSpecificOutput" : {"paymentProductId" : 1}},"status" : "REJECTED", + "statusOutput":{"errors" : [ {"code" : "21000120","requestId" : "978755","propertyName" : + "cardPaymentMethodSpecificInput.card.expiryDate","message" : + "cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT", + "httpStatusCode" : 400} ],"isCancellable" : false,"statusCategory" : + "UNSUCCESSFUL","statusCode" : 100,"statusCodeChangeDateTime" : "20180118111508", + "isAuthorized" : false,"isRefundable" : false}}}}/, headers: [ {"Date", "Thu, 18 Jan 2018 10:15:08 GMT"}, {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, @@ -95,7 +140,9 @@ defmodule Gringotts.Gateways.GlobalCollectMock do {:ok, %HTTPoison.Response{ body: - "{\n \"errorId\" : \"1dbef568-ed86-4c8d-a3c3-74ced258d5a2\",\n \"errors\" : [ {\n \"code\" : \"1099\",\n \"id\" : \"INVALID_VALUE\",\n \"category\" : \"CONNECT_PLATFORM_ERROR\",\n \"message\" : \"INVALID_VALUE: '50.3' is not a valid value for field 'amount'\",\n \"httpStatusCode\" : 400\n } ]\n}", + ~s/{"errorId" : "1dbef568-ed86-4c8d-a3c3-74ced258d5a2","errors" : [ {"code" : "1099","id" : + "INVALID_VALUE", "category" : "CONNECT_PLATFORM_ERROR","message" : + "INVALID_VALUE: '50.3' is not a valid value for field 'amount'","httpStatusCode" : 400} ]}/, headers: [ {"Date", "Tue, 23 Jan 2018 11:18:11 GMT"}, {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, @@ -113,7 +160,8 @@ defmodule Gringotts.Gateways.GlobalCollectMock do {:ok, %HTTPoison.Response{ body: - "{\n \"errorId\" : \"b6ba00d2-8f11-4822-8f32-c6d0a4d8793b\",\n \"errors\" : [ {\n \"code\" : \"300450\",\n \"message\" : \"ORDER WITHOUT REFUNDABLE PAYMENTS\",\n \"httpStatusCode\" : 400\n } ]\n}", + ~s/{ "errorId" : "b6ba00d2-8f11-4822-8f32-c6d0a4d8793b", "errors" : [ {"code" : "300450", + "message" : "ORDER WITHOUT REFUNDABLE PAYMENTS", "httpStatusCode" : 400 } ]}/, headers: [ {"Date", "Wed, 24 Jan 2018 05:33:56 GMT"}, {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, @@ -131,8 +179,15 @@ defmodule Gringotts.Gateways.GlobalCollectMock do def test_for_capture_with_valid_paymentid do {:ok, %HTTPoison.Response{ - body: - "{\n \"payment\" : {\n \"id\" : \"000000122600000000650000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 50,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"1218\"\n }\n }\n },\n \"status\" : \"CAPTURE_REQUESTED\",\n \"statusOutput\" : {\n \"isCancellable\" : true,\n \"statusCategory\" : \"PENDING_CONNECT_OR_3RD_PARTY\",\n \"statusCode\" : 800,\n \"statusCodeChangeDateTime\" : \"20180123140826\",\n \"isAuthorized\" : true,\n \"isRefundable\" : false\n }\n }\n}", + body: ~s/{ "payment" : {"id" : "000000122600000000650000100001", "paymentOutput" : { + "amountOfMoney" :{"amount" : 50,"currencyCode" : "USD"},"references" : {"paymentReference" + : "0"},"paymentMethod" : "card","cardPaymentMethodSpecificOutput" : {"paymentProductId" : + 1,"authorisationCode" : "OK1131","fraudResults" : {"fraudServiceResult" : "no-advice", + "avsResult" : "0","cvvResult" : "0"},"card" :{"cardNumber" : "************7977", + "expiryDate" : "1218"}}},"status" : "CAPTURE_REQUESTED","statusOutput" : + {"isCancellable" : true,"statusCategory" : "PENDING_CONNECT_OR_3RD_PARTY", + "statusCode" : 800,"statusCodeChangeDateTime" : "20180123140826","isAuthorized" : true, + "isRefundable" : false} }}/, headers: [ {"Date", "Tue, 23 Jan 2018 13:08:26 GMT"}, {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, @@ -149,8 +204,9 @@ defmodule Gringotts.Gateways.GlobalCollectMock do def test_for_capture_with_invalid_paymentid do {:ok, %HTTPoison.Response{ - body: - "{\n \"errorId\" : \"ccb99804-0240-45b6-bb28-52aaae59d71b\",\n \"errors\" : [ {\n \"code\" : \"1002\",\n \"id\" : \"UNKNOWN_PAYMENT_ID\",\n \"category\" : \"CONNECT_PLATFORM_ERROR\",\n \"propertyName\" : \"paymentId\",\n \"message\" : \"UNKNOWN_PAYMENT_ID\",\n \"httpStatusCode\" : 404\n } ]\n}", + body: ~s/{ "errorId" : "ccb99804-0240-45b6-bb28-52aaae59d71b", "errors" : [ + {"code" : "1002","id" :"UNKNOWN_PAYMENT_ID","category" : "CONNECT_PLATFORM_ERROR", + "propertyName" : "paymentId","message": "UNKNOWN_PAYMENT_ID","httpStatusCode" :404}]}/, headers: [ {"Date", "Tue, 23 Jan 2018 12:25:59 GMT"}, {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, @@ -167,7 +223,15 @@ defmodule Gringotts.Gateways.GlobalCollectMock do {:ok, %HTTPoison.Response{ body: - "{\n \"payment\" : {\n \"id\" : \"000000122600000000870000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 50,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"1218\"\n }\n }\n },\n \"status\" : \"CANCELLED\",\n \"statusOutput\" : {\n \"isCancellable\" : false,\n \"statusCategory\" : \"UNSUCCESSFUL\",\n \"statusCode\" : 99999,\n \"statusCodeChangeDateTime\" : \"20180124064204\",\n \"isAuthorized\" : false,\n \"isRefundable\" : false\n }\n }\n}", + ~s/{ "payment" : {"id" : "000000122600000000870000100001","paymentOutput" : {"amountOfMoney" + :{"amount" : 50,"currencyCode" : "USD"},"references" : {"paymentReference" : "0"}, + "paymentMethod" : "card","cardPaymentMethodSpecificOutput" : {"paymentProductId" : 1, + "authorisationCode" : "OK1131","fraudResults" : {"fraudServiceResult" : "no-advice", + "avsResult" : "0","cvvResult" : "0"},"card" :{"cardNumber" : "************7977", + "expiryDate" : "1218"}}},"status" : "CANCELLED","statusOutput":{"isCancellable" : + false,"statusCategory" : "UNSUCCESSFUL","statusCode" : 99999, + "statusCodeChangeDateTime" : "20180124064204","isAuthorized" : false,"isRefundable" : + false}}}/, headers: [ {"Date", "Wed, 24 Jan 2018 05:42:04 GMT"}, {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, From b1b78917112c5a58859844564c9d692e6cbe6dd0 Mon Sep 17 00:00:00 2001 From: Ashish Singh Date: Sun, 22 Apr 2018 17:58:24 +0530 Subject: [PATCH 16/24] Changes for release 1.1.0 * Bumped version to 1.1.0 * Not running integration tests on travis * Added Changelog for release 1.1.0(refer CHANGELOG.md) --- .travis.yml | 5 +++-- CHANGELOG.md | 26 +++++++++++++++----------- mix.exs | 2 +- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/.travis.yml b/.travis.yml index a238ae93..d12dcef4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ script: - set -e - MIX_ENV=test mix format --check-formatted - set +e - - mix coveralls.json --include=integration + - mix coveralls.json after_script: - bash <(curl -s https://codecov.io/bash) - bash .scripts/inch_report.sh @@ -19,10 +19,11 @@ matrix: include: - elixir: "1.5.3" script: - - mix coveralls.json --include=integration + - mix coveralls.json - elixir: "1.6.2" notifications: email: recipients: - ananya95+travis@gmail.com + - ashish+travis@aviabird.com diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a1f16ad..63fc4d94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,21 +1,25 @@ -# [`v1.1.0-alpha`][tag-1_1_0_alpha] +# [`v1.1.0`][tag-1_1_0] (2018-04-22) ## Added -* [`ISS`][iss#80] [`PR`][pr#78] -Add a `Mix` task that generates a barebones gateway implementation and test suite. +* **api** Introduces a `Money` protocol ([#71][pr#71]) +* **core** Introduces Response.t ([#119][pr#91]) +* **development** Adds a useful mix task gringotts.new ([#78][pr#78]) +* **docs** Adds changelog, contributing guide ([#117][pr#117]) ## Changed -* [`ISS`][iss#62] [`PR`][pr#71] [`PR`][pr#86] -Deprecate use of `floats` for money amounts, introduce the `Gringotts.Money` protocol. +* **api** Deprecates use of `floats` for money amounts, check issue [#62][iss#62] ([#71][pr#71]) +* **core** Removes payment worker, no application, no worker now after josevalim [pointed it][jose-feedback] ([#118][pr#118]) [iss#62]: https://github.com/aviabird/gringotts/issues/62 -[iss#80]: https://github.com/aviabird/gringotts/issues/80 - [pr#71]: https://github.com/aviabird/gringotts/pulls/71 +[pr#118]: https://github.com/aviabird/gringotts/pulls/118 +[pr#91]: https://github.com/aviabird/gringotts/pulls/91 +[pr#117]: https://github.com/aviabird/gringotts/pulls/117 [pr#78]:https://github.com/aviabird/gringotts/pulls/78 [pr#86]:https://github.com/aviabird/gringotts/pulls/86 +[jose-feedback]:https://elixirforum.com/t/gringotts-a-complete-payment-library-for-elixir-and-phoenix-framework/11054/41 # [`v1.0.2`][tag-1_0_2] @@ -44,9 +48,9 @@ Deprecate use of `floats` for money amounts, introduce the `Gringotts.Money` pro - MONEI - Paymill - WireCard - - CAMS + - CAMSa -[tag-1_1_0_alpha]: https://github.com/aviabird/gringotts/releases/tag/v1.1.0-alpha -[tag-1_0_2]: https://github.com/aviabird/gringotts/releases/tag/v1.0.2 -[tag-1_0_1]: https://github.com/aviabird/gringotts/releases/tag/v1.0.1 +[tag-1_1_0]: https://github.com/aviabird/gringotts/compare/1.1.0...1.0.2 +[tag-1_0_2]: https://github.com/aviabird/gringotts/compare/1.0.2...1.0.1 +[tag-1_0_1]: https://github.com/aviabird/gringotts/compare/1.0.1...1.0.0 [tag-1_0_0]: https://github.com/aviabird/gringotts/releases/tag/v1.0.0 diff --git a/mix.exs b/mix.exs index 244a6405..2a4f2266 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Gringotts.Mixfile do def project do [ app: :gringotts, - version: "1.0.2", + version: "1.1.0", description: description(), package: [ contributors: ["Aviabird Technologies"], From 59fbac54eceaaf90046314fbb793698b437ec541 Mon Sep 17 00:00:00 2001 From: Ashish Singh Date: Sun, 22 Apr 2018 19:05:28 +0530 Subject: [PATCH 17/24] Update CHANGELOG.md --- CHANGELOG.md | 47 ++++++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63fc4d94..f5b42e88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,18 @@ -# [`v1.1.0`][tag-1_1_0] (2018-04-22) +# Changelog -## Added +## [`v1.1.0`][tag-1_1_0] (2018-04-22) -* **api** Introduces a `Money` protocol ([#71][pr#71]) -* **core** Introduces Response.t ([#119][pr#91]) -* **development** Adds a useful mix task gringotts.new ([#78][pr#78]) -* **docs** Adds changelog, contributing guide ([#117][pr#117]) +### Added -## Changed +* **api:** Introduces a `Money` protocol ([#71][pr#71]) +* **core:** Introduces Response.t ([#119][pr#91]) +* **development:** Adds a useful mix task gringotts.new ([#78][pr#78]) +* **docs:** Adds changelog, contributing guide ([#117][pr#117]) -* **api** Deprecates use of `floats` for money amounts, check issue [#62][iss#62] ([#71][pr#71]) -* **core** Removes payment worker, no application, no worker now after josevalim [pointed it][jose-feedback] ([#118][pr#118]) +### Changed + +* **api:** Deprecates use of `floats` for money amounts, check issue [#62][iss#62] ([#71][pr#71]) +* **core:** Removes payment worker, no application, no worker now after josevalim [pointed it][jose-feedback] ([#118][pr#118]) [iss#62]: https://github.com/aviabird/gringotts/issues/62 [pr#71]: https://github.com/aviabird/gringotts/pulls/71 @@ -21,29 +23,32 @@ [pr#86]:https://github.com/aviabird/gringotts/pulls/86 [jose-feedback]:https://elixirforum.com/t/gringotts-a-complete-payment-library-for-elixir-and-phoenix-framework/11054/41 -# [`v1.0.2`][tag-1_0_2] -## Added +## [`v1.0.2`][tag-1_0_2] (2017-12-27) + +### Added * New Gateway: **Trexle** -## Changed +### Changed -* Reduced arity of public API calls by 1 +* **api:** Reduced arity of public API calls by 1 - No need to pass the name of the `worker` as argument. -# [`v1.0.1`][tag-1_0_1] +## [`v1.0.1`][tag-1_0_1] (2017-12-23) + +### Added -## Added +* **docs:** Improved documentation - made consistent accross gateways +* **tests:** Improved test coverage -* Improved documentation - made consistent accross gateways -* Improved test coverage +## [`v1.0.0`][tag-1_0_0] (2017-12-20) -# [`v1.0.0`][tag-1_0_0] +### Added -* Initial public API release. -* Single worker architecture, config fetched from `config.exs` -* Supported Gateways: +* **api:** Initial public API release. +* **core:** Single worker architecture, config fetched from `config.exs` +* **api:** Supported Gateways: - Stripe - MONEI - Paymill From f7850768484ce75725337e953791069e541810f4 Mon Sep 17 00:00:00 2001 From: Anant Date: Mon, 23 Apr 2018 18:33:12 +0530 Subject: [PATCH 18/24] [Paymill] refactor and tests (#152) * New public methods: refund * `store` has been removed because Paymill does not have a direct API integration for card token generation * Improved docs and uses Money protocol Refactors ---------- * refactored add_amount -> amount_params * updated tests and clubbed a few parse clauses * refactored commit and response functions Tests ------ * Paymill mock (#159) * Added mock test cases for paymill. * Configured mock files to get compiled in `:test` env. * Moved paymill responses in mocks folder. --- lib/gringotts/gateways/paymill.ex | 657 +++++++++--------- mix.exs | 14 +- mix.lock | 2 + test/gateways/authorize_net_test.exs | 1 - test/gateways/cams_test.exs | 11 +- test/gateways/global_collect_test.exs | 1 - test/gateways/paymill_test.exs | 215 ++++++ test/gateways/trexle_test.exs | 1 - test/gateways/wire_card_test.exs | 2 - test/integration/gateways/paymill_test.exs | 85 +++ ...ize_net_mock.exs => authorize_net_mock.ex} | 1 + test/mocks/{cams_mock.exs => cams_mock.ex} | 1 + ...ollect_mock.exs => global_collect_mock.ex} | 2 + test/mocks/paymill_mock.ex | 136 ++++ .../mocks/{trexle_mock.exs => trexle_mock.ex} | 11 +- 15 files changed, 805 insertions(+), 335 deletions(-) create mode 100644 test/gateways/paymill_test.exs create mode 100644 test/integration/gateways/paymill_test.exs rename test/mocks/{authorize_net_mock.exs => authorize_net_mock.ex} (99%) rename test/mocks/{cams_mock.exs => cams_mock.ex} (99%) rename test/mocks/{global_collect_mock.exs => global_collect_mock.ex} (99%) create mode 100644 test/mocks/paymill_mock.ex rename test/mocks/{trexle_mock.exs => trexle_mock.ex} (93%) diff --git a/lib/gringotts/gateways/paymill.ex b/lib/gringotts/gateways/paymill.ex index 5cf177e2..e9ab3015 100644 --- a/lib/gringotts/gateways/paymill.ex +++ b/lib/gringotts/gateways/paymill.ex @@ -1,8 +1,8 @@ defmodule Gringotts.Gateways.Paymill do @moduledoc """ - An Api Client for the [PAYMILL](https://www.paymill.com/) gateway. + [PAYMILL][home] gateway implementation. - For refernce see [PAYMILL's API (v2.1) documentation](https://developers.paymill.com/API/index) + For refernce see [PAYMILL's API (v2.1) documentation][docs]. The following features of PAYMILL are implemented: @@ -11,388 +11,399 @@ defmodule Gringotts.Gateways.Paymill do | Authorize | `authorize/3` | | Capture | `capture/3` | | Purchase | `purchase/3` | + | Refund | `refund/3` | | Void | `void/2` | - Following fields are required for config + ## The `opts` argument - | Config Parameter | PAYMILL secret | - | private_key | **your_private_key** | - | public_key | **your_public_key** | + Most `Gringotts` API calls accept an optional `keyword` list `opts` to supply + optional arguments for transactions with the PAYMILL gateway. **Currently, no + optional params are supported.** - Your application config must include 'private_key', 'public_key' + ## Registering your PAYMILL account at `Gringotts` + + After [making an account on PAYMILL][dashboard], head to the dashboard and find + your account "secrets". + + Here's how the secrets map to the required configuration parameters for PAYMILL: + + | Config parameter | PAYMILL secret | + | ------- | ---- | + | `:private_key` | **Private Key** | + | `:public_key` | **Public Key** | + + Your Application config **must include the `:private_key`, `:public_key` + fields** and would look something like this: config :gringotts, Gringotts.Gateways.Paymill, - private_key: "your_privat_key", - public_key: "your_public_key" + private_key: "your_secret_private_key", + public_key: "your_secret_public_key" + + ## Scope of this module + + * PAYMILL processes money in the sub-divided unit of currency (ie, in case of + USD it works in cents). + * PAYMILL does not offer direct API integration for [PCI DSS][pci-dss] + compliant merchants, everyone must use PAYMILL as if they are not PCI + compliant. + * To use their product, a merchant (aka user of this library) would have to + use their [Bridge (js integration)][bridge] (or equivalent) in your + application frontend to collect Credit/Debit Card data. + * This would obtain a unique `card_token` at the client-side which can be used + by this module for various operations like `authorize/3` and `purchase/3`. + + [bridge]: https://developers.paymill.com/guides/reference/paymill-bridge.html + + ## Supported countries + As a PAYMILL merchant you can accept payments from around the globe. For more details + refer to [Paymill country support][country-support]. + + ## Supported currencies + Your transactions will be processed in your native currency. For more information + refer to [Paymill currency support][currency-support]. + + ## Following the examples + + 1. First, set up a sample application and configure it to work with PAYMILL. + - You could do that from scratch by following our [Getting Started][gs] guide. + - To save you time, we recommend [cloning our example repo][example-repo] + 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-paymill-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): + ``` + iex> alias Gringotts.{Response, CreditCard, Gateways.Paymill} + iex> amount = Money.new(4200, :EUR) + ``` + + We'll be using these in the examples below. + + [home]: https://paymill.com + [docs]: https://developers.paymill.com + [dashboard]: https://app.paymill.com/user/register + [gs]: https://github.com/aviabird/gringotts/wiki + [example-repo]: https://github.com/aviabird/gringotts_example + [currency-support]: https://www.paymill.com/en/faq/in-which-currency-will-my-transactions-be-processed-and-payout-in + [country-support]: https://www.paymill.com/en/faq/which-countries-is-paymill-available-in + [pci-dss]: https://www.paymill.com/en/pci-dss """ - use Gringotts.Gateways.Base - alias Gringotts.{CreditCard, Address, Response} - alias Gringotts.Gateways.Paymill.ResponseHandler, as: ResponseParser + use Gringotts.Gateways.Base use Gringotts.Adapter, required_config: [:private_key, :public_key] - @home_page "https://paymill.com" - @money_format :cents - @default_currency "EUR" - @live_url "https://api.paymill.com/v2.1/" + alias Gringotts.{Response, Money} + + @base_url "https://api.paymill.com/v2.1/" @headers [{"Content-Type", "application/x-www-form-urlencoded"}] + @response_code %{ + 10_001 => "Undefined response", + 10_002 => "Waiting for something", + 11_000 => "Retry request at a later time", + 20_000 => "Operation successful", + 20_100 => "Funds held by acquirer", + 20_101 => "Funds held by acquirer because merchant is new", + 20_200 => "Transaction reversed", + 20_201 => "Reversed due to chargeback", + 20_202 => "Reversed due to money-back guarantee", + 20_203 => "Reversed due to complaint by buyer", + 20_204 => "Payment has been refunded", + 20_300 => "Reversal has been canceled", + 22_000 => "Initiation of transaction successful", + 30_000 => "Transaction still in progress", + 30_100 => "Transaction has been accepted", + 31_000 => "Transaction pending", + 31_100 => "Pending due to address", + 31_101 => "Pending due to uncleared eCheck", + 31_102 => "Pending due to risk review", + 31_103 => "Pending due regulatory review", + 31_104 => "Pending due to unregistered/unconfirmed receiver", + 31_200 => "Pending due to unverified account", + 31_201 => "Pending due to non-captured funds", + 31_202 => "Pending due to international account (accept manually)", + 31_203 => "Pending due to currency conflict (accept manually)", + 31_204 => "Pending due to fraud filters (accept manually)", + 40_000 => "Problem with transaction data", + 40_001 => "Problem with payment data", + 40_002 => "Invalid checksum", + 40_100 => "Problem with credit card data", + 40_101 => "Problem with CVV", + 40_102 => "Card expired or not yet valid", + 40_103 => "Card limit exceeded", + 40_104 => "Card is not valid", + 40_105 => "Expiry date not valid", + 40_106 => "Credit card brand required", + 40_200 => "Problem with bank account data", + 40_201 => "Bank account data combination mismatch", + 40_202 => "User authentication failed", + 40_300 => "Problem with 3-D Secure data", + 40_301 => "Currency/amount mismatch", + 40_400 => "Problem with input data", + 40_401 => "Amount too low or zero", + 40_402 => "Usage field too long", + 40_403 => "Currency not allowed", + 40_410 => "Problem with shopping cart data", + 40_420 => "Problem with address data", + 40_500 => "Permission error with acquirer API", + 40_510 => "Rate limit reached for acquirer API", + 42_000 => "Initiation of transaction failed", + 42_410 => "Initiation of transaction expired", + 50_000 => "Problem with back end", + 50_001 => "Country blacklisted", + 50_002 => "IP address blacklisted", + 50_004 => "Live mode not allowed", + 50_005 => "Insufficient permissions (API key)", + 50_100 => "Technical error with credit card", + 50_101 => "Error limit exceeded", + 50_102 => "Card declined", + 50_103 => "Manipulation or stolen card", + 50_104 => "Card restricted", + 50_105 => "Invalid configuration data", + 50_200 => "Technical error with bank account", + 50_201 => "Account blacklisted", + 50_300 => "Technical error with 3-D Secure", + 50_400 => "Declined because of risk issues", + 50_401 => "Checksum was wrong", + 50_402 => "Bank account number was invalid (formal check)", + 50_403 => "Technical error with risk check", + 50_404 => "Unknown error with risk check", + 50_405 => "Unknown bank code", + 50_406 => "Open chargeback", + 50_407 => "Historical chargeback", + 50_408 => "Institution / public bank account (NCA)", + 50_409 => "KUNO/Fraud", + 50_410 => "Personal Account Protection (PAP)", + 50_420 => "Rejected due to acquirer fraud settings", + 50_430 => "Rejected due to acquirer risk settings", + 50_440 => "Failed due to restrictions with acquirer account", + 50_450 => "Failed due to restrictions with user account", + 50_500 => "General timeout", + 50_501 => "Timeout on side of the acquirer", + 50_502 => "Risk management transaction timeout", + 50_600 => "Duplicate operation", + 50_700 => "Cancelled by user", + 50_710 => "Failed due to funding source", + 50_711 => "Payment method not usable, use other payment method", + 50_712 => "Limit of funding source was exceeded", + 50_713 => "Means of payment not reusable (canceled by user)", + 50_714 => "Means of payment not reusable (expired)", + 50_720 => "Rejected by acquirer", + 50_730 => "Transaction denied by merchant", + 50_800 => "Preauthorisation failed", + 50_810 => "Authorisation has been voided", + 50_820 => "Authorisation period expired" + } + @doc """ - Authorize a card with particular amount and return a token in response + Performs a (pre) Authorize operation. + + The authorization validates the `card` details for `token` 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. - ### Example - amount = 100 + The authorization token is available in the `Response.id` field. - card = %CreditCard{ - first_name: "Sagar", - last_name: "Karwande", - number: "4111111111111111", - month: 12, - year: 2018, - verification_code: 123 - } + ## Example - options = [] + The following example shows how one would (pre) authorize a payment of €42 on + a sample `token`. + ``` + iex> amount = Money.new(4200, :EUR) + iex> card_token = "tok_XXXXXXXXXXXXXXXXXXXXXXXXXXXX" - iex> Gringotts.authorize(Gringotts.Gateways.Paymill, amount, card, options) + iex> {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.Paymill, amount, card_token, opts) + iex> auth_result.id # This is the preauth-id + ``` """ - @spec authorize(number, String.t() | CreditCard.t(), Keyword) :: {:ok | :error, Response} - def authorize(amount, card_or_token, options) do - Keyword.put(options, :money, amount) - action_with_token(:authorize, amount, card_or_token, options) + + @spec authorize(Money.t(), String.t(), keyword) :: {:ok | :error, Response.t()} + def authorize(amount, card_token, opts) do + params = [{:token, card_token} | amount_params(amount)] + commit(:post, "preauthorizations", params, opts) end @doc """ - Purchase with a card + Captures a pre-authorized `amount`. - ### Example - amount = 100 + `amount` is transferred to the merchant account by PAYMILL when it is smaller or + equal to the amount used in the pre-authorization referenced by `preauth_id`. - card = %CreditCard{ - first_name: "Sagar", - last_name: "Karwande", - number: "4111111111111111", - month: 12, - year: 2018, - verification_code: 123 - } + ## Note - options = [] + PAYMILL allows partial captures and unlike many other gateways, and releases + any remaining amount back to the payment source. + > Thus, the same pre-authorisation ID **cannot** be used to perform multiple + captures. - iex> Gringotts.purchase(Gringotts.Gateways.Paymill, amount, card, options) - """ - @spec purchase(number, CreditCard.t(), Keyword) :: {:ok | :error, Response} - def purchase(amount, card, options) do - Keyword.put(options, :money, amount) - action_with_token(:purchase, amount, card, options) - end - - @doc """ - Capture a particular amount with authorization token + ## Example - ### Example - amount = 100 + The following example shows how one would (partially) capture a previously + authorized a payment worth €42 by referencing the obtained authorization `id`. - token = "preauth_14c7c5268eb155a599f0" - - options = [] - - iex> Gringotts.capture(Gringotts.Gateways.Paymill, token, amount, options) + ``` + iex> amount = Money.new(4200, :EUR) + iex> preauth_id = auth_result.id + # preauth_id = "some_authorization_id" + iex> Gringotts.capture(Gringotts.Gateways.Paymill, preauth_id, amount, opts) + ``` """ - @spec capture(String.t(), number, Keyword) :: {:ok | :error, Response} - def capture(authorization, amount, options) do - post = add_amount([], amount, options) ++ [{"preauthorization", authorization}] - - commit(:post, "transactions", post, options) + @spec capture(String.t(), Money.t(), keyword) :: {:ok | :error, Response.t()} + def capture(id, amount, opts) do + params = [{:preauthorization, id} | amount_params(amount)] + commit(:post, "transactions", params, opts) end @doc """ - Voids a particular authorized amount + Transfers `amount` from the customer to the merchant. - ### Example - token = "preauth_14c7c5268eb155a599f0" + PAYMILL attempts to process a purchase on behalf of the customer, by debiting + `amount` from the customer's account by charging the customer's `card` via `token`. - options = [] + ## Example - iex> Gringotts.void(Gringotts.Gateways.Paymill, token, options) - """ - @spec void(String.t(), Keyword) :: {:ok | :error, Response} - def void(authorization, options) do - commit(:delete, "preauthorizations/#{authorization}", [], options) - end + The following example shows how one would process a payment worth €42 in + one-shot, without (pre) authorization. - @doc false - @spec authorize_with_token(number, String.t(), Keyword) :: term - def authorize_with_token(money, card_token, options) do - post = add_amount([], money, options) ++ [{"token", card_token}] + ``` + iex> amount = Money.new(4200, :EUR) + iex> token = "tok_XXXXXXXXXXXXXXXXXXXXXXXXXXXX" - commit(:post, "preauthorizations", post, options) + iex> {:ok, purchase_result} = Gringotts.purchase(Gringotts.Gateways.Paymill, amount, token, opts) + ``` + """ + @spec purchase(Money.t(), String.t(), keyword) :: {:ok | :error, Response.t()} + def purchase(amount, card_token, opts) do + param = [{:token, card_token} | amount_params(amount)] + commit(:post, "transactions", param, opts) end - @doc false - @spec purchase_with_token(number, String.t(), Keyword) :: term - def purchase_with_token(money, card_token, options) do - post = add_amount([], money, options) ++ [{"token", card_token}] + @doc """ + Refunds the `amount` to the customer's account with reference to a prior transfer. - commit(:post, "transactions", post, options) - end + PAYMILL processes a full or partial refund worth `amount`, where `transaction_id` + references a previous `purchase/3` or `capture/3` result. - @spec save_card(CreditCard.t(), Keyword) :: Response - defp save_card(card, options) do - {:ok, %HTTPoison.Response{body: response}} = - HTTPoison.get( - get_save_card_url(), - get_headers(options), - params: get_save_card_params(card, options) - ) + Multiple partial refunds are allowed on the same `transaction_id` till all the + captured/purchased amount has been refunded. - parse_card_response(response) - end + ## Example - @spec save(CreditCard.t(), Keyword) :: Response - defp save(card, options) do - save_card(card, options) + The following example shows how one would refund a previous purchase (and + similarily for captures). + ``` + iex> transaction_id = purchase_result.id + iex> amount = Money.new(4200, :EUR) + iex> Gringotts.refund(Gringotts.Gateways.Paymill, amount, transaction_id) + ``` + """ + @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response.t()} + def refund(amount, id, opts) do + {_, int_value, _} = Money.to_integer(amount) + commit(:post, "refunds/#{id}", [amount: int_value], opts) end - defp action_with_token(action, amount, "tok_" <> id = card_token, options) do - apply(__MODULE__, String.to_atom("#{action}_with_token"), [amount, card_token, options]) - end + @doc """ + Voids the referenced authorization. - defp action_with_token(action, amount, %CreditCard{} = card, options) do - {:ok, response} = save_card(card, options) - card_token = get_token(response) + Attempts a reversal of the a previous `authorize/3` referenced by + `preauth_id`. - apply(__MODULE__, String.to_atom("#{action}_with_token"), [amount, card_token, options]) - end + ## Example - defp get_save_card_params(card, options) do - [ - {"transaction.mode", "CONNECTOR_TEST"}, - {"channel.id", get_config(:public_key, options)}, - {"jsonPFunction", "jsonPFunction"}, - {"account.number", card.number}, - {"account.expiry.month", card.month}, - {"account.expiry.year", card.year}, - {"account.verification", card.verification_code}, - {"account.holder", CreditCard.full_name(card)}, - {"presentation.amount3D", get_amount(options)}, - {"presentation.currency3D", get_currency(options)} - ] + The following example shows how one would void a previous authorization. + ``` + iex> preauth_id = auth_result.id + iex> Gringotts.void(Gringotts.Gateways.Paymill, preauth_id) + ``` + """ + @spec void(String.t(), keyword) :: {:ok | :error, Response.t()} + def void(id, opts) do + commit(:delete, "preauthorizations/#{id}", [], opts) end - defp get_headers(options) do - @headers ++ set_username(options) + defp commit(method, endpoint, params, opts) do + method + |> HTTPoison.request(base_url(opts) <> endpoint, {:form, params}, headers(opts)) + |> respond() end - defp add_amount(post, money, options) do - post ++ [{"amount", money}, {"currency", @default_currency}] + @response_code_paths [ + ~w[transaction response_code], + ~w[data response_code], + ~w[data transaction response_code] + ] + @token_paths [~w[id], ~w[data id]] + @reason_paths [~w[error], ~w[exception]] + @fraud_paths [ + ~w[transaction is_fraud], + ~w[data transaction is_fraud], + ~w[data transaction is_markable_as_fraud], + ~w[data is_markable_as_fraud] + ] + + defp get_either(collection, paths) do + paths + |> Stream.map(&get_in(collection, &1)) + |> Enum.find(fn x -> x != nil end) end - defp set_username(options) do - [{"Authorization", "Basic #{Base.encode64(get_config(:private_key, options))}"}] + defp respond({:ok, %{status_code: 200, body: body}}) do + case Poison.decode(body) do + {:ok, parsed_resp} -> + gateway_code = get_either(parsed_resp, @response_code_paths) + + status = if gateway_code in [20_000, 50_810], do: :ok, else: :error + + {status, + %Response{ + id: get_either(parsed_resp, @token_paths), + token: parsed_resp["transaction"]["identification"]["uniqueId"], + status_code: 200, + gateway_code: gateway_code, + reason: get_either(parsed_resp, @reason_paths), + message: @response_code[gateway_code], + raw: body, + fraud_review: get_either(parsed_resp, @fraud_paths) + }} + + :error -> + {:error, + %Response{status_code: 200, raw: body, reason: "could not parse paymill response"}} + end end - defp get_save_card_url(), do: "https://test-token.paymill.com/" - - defp parse_card_response(response) do - response - |> String.replace(~r/jsonPFunction\(/, "") - |> String.replace(~r/\)/, "") - |> Poison.decode() + defp respond({:ok, %{status_code: status_code, body: body}}) do + {:error, + %Response{ + status_code: status_code, + raw: body + }} end - defp get_currency(options), do: options[:currency] || @default_currency - - defp get_amount(options), do: options[:money] - - defp get_token(response) do - get_in(response, ["transaction", "identification", "uniqueId"]) + defp respond({:error, %HTTPoison.Error{} = error}) do + { + :error, + Response.error( + reason: "network related failure", + message: "HTTPoison says '#{error.reason}' [ID: #{error.id || "nil"}]" + ) + } end - defp commit(method, action, parameters \\ nil, options) do - method - |> HTTPoison.request(@live_url <> action, {:form, parameters}, get_headers(options), []) - |> ResponseParser.parse() + defp headers(opts) do + [ + {"Authorization", "Basic #{Base.encode64(get_in(opts, [:config, :private_key]))}"} + | @headers + ] end - defp get_config(key, options) do - get_in(options, [:config, key]) + defp amount_params(money) do + {currency, int_value, _} = Money.to_integer(money) + [amount: int_value, currency: currency] end - @moduledoc false - defmodule ResponseHandler do - alias Gringotts.Response - - @response_code %{ - 10_001 => "Undefined response", - 10_002 => "Waiting for something", - 11_000 => "Retry request at a later time", - 20_000 => "Operation successful", - 20_100 => "Funds held by acquirer", - 20_101 => "Funds held by acquirer because merchant is new", - 20_200 => "Transaction reversed", - 20_201 => "Reversed due to chargeback", - 20_202 => "Reversed due to money-back guarantee", - 20_203 => "Reversed due to complaint by buyer", - 20_204 => "Payment has been refunded", - 20_300 => "Reversal has been canceled", - 22_000 => "Initiation of transaction successful", - 30_000 => "Transaction still in progress", - 30_100 => "Transaction has been accepted", - 31_000 => "Transaction pending", - 31_100 => "Pending due to address", - 31_101 => "Pending due to uncleared eCheck", - 31_102 => "Pending due to risk review", - 31_103 => "Pending due regulatory review", - 31_104 => "Pending due to unregistered/unconfirmed receiver", - 31_200 => "Pending due to unverified account", - 31_201 => "Pending due to non-captured funds", - 31_202 => "Pending due to international account (accept manually)", - 31_203 => "Pending due to currency conflict (accept manually)", - 31_204 => "Pending due to fraud filters (accept manually)", - 40_000 => "Problem with transaction data", - 40_001 => "Problem with payment data", - 40_002 => "Invalid checksum", - 40_100 => "Problem with credit card data", - 40_101 => "Problem with CVV", - 40_102 => "Card expired or not yet valid", - 40_103 => "Card limit exceeded", - 40_104 => "Card is not valid", - 40_105 => "Expiry date not valid", - 40_106 => "Credit card brand required", - 40_200 => "Problem with bank account data", - 40_201 => "Bank account data combination mismatch", - 40_202 => "User authentication failed", - 40_300 => "Problem with 3-D Secure data", - 40_301 => "Currency/amount mismatch", - 40_400 => "Problem with input data", - 40_401 => "Amount too low or zero", - 40_402 => "Usage field too long", - 40_403 => "Currency not allowed", - 40_410 => "Problem with shopping cart data", - 40_420 => "Problem with address data", - 40_500 => "Permission error with acquirer API", - 40_510 => "Rate limit reached for acquirer API", - 42_000 => "Initiation of transaction failed", - 42_410 => "Initiation of transaction expired", - 50_000 => "Problem with back end", - 50_001 => "Country blacklisted", - 50_002 => "IP address blacklisted", - 50_004 => "Live mode not allowed", - 50_005 => "Insufficient permissions (API key)", - 50_100 => "Technical error with credit card", - 50_101 => "Error limit exceeded", - 50_102 => "Card declined", - 50_103 => "Manipulation or stolen card", - 50_104 => "Card restricted", - 50_105 => "Invalid configuration data", - 50_200 => "Technical error with bank account", - 50_201 => "Account blacklisted", - 50_300 => "Technical error with 3-D Secure", - 50_400 => "Declined because of risk issues", - 50_401 => "Checksum was wrong", - 50_402 => "Bank account number was invalid (formal check)", - 50_403 => "Technical error with risk check", - 50_404 => "Unknown error with risk check", - 50_405 => "Unknown bank code", - 50_406 => "Open chargeback", - 50_407 => "Historical chargeback", - 50_408 => "Institution / public bank account (NCA)", - 50_409 => "KUNO/Fraud", - 50_410 => "Personal Account Protection (PAP)", - 50_420 => "Rejected due to acquirer fraud settings", - 50_430 => "Rejected due to acquirer risk settings", - 50_440 => "Failed due to restrictions with acquirer account", - 50_450 => "Failed due to restrictions with user account", - 50_500 => "General timeout", - 50_501 => "Timeout on side of the acquirer", - 50_502 => "Risk management transaction timeout", - 50_600 => "Duplicate operation", - 50_700 => "Cancelled by user", - 50_710 => "Failed due to funding source", - 50_711 => "Payment method not usable, use other payment method", - 50_712 => "Limit of funding source was exceeded", - 50_713 => "Means of payment not reusable (canceled by user)", - 50_714 => "Means of payment not reusable (expired)", - 50_720 => "Rejected by acquirer", - 50_730 => "Transaction denied by merchant", - 50_800 => "Preauthorisation failed", - 50_810 => "Authorisation has been voided", - 50_820 => "Authorisation period expired" - } - - def parse({:ok, %HTTPoison.Response{body: body, status_code: 200}}) do - body = Poison.decode!(body) - parse_body(body) - end - - def parse({:ok, %HTTPoison.Response{body: body, status_code: 400}}) do - body = Poison.decode!(body) - - [] - |> set_params(body) - end - - def parse({:ok, %HTTPoison.Response{body: body, status_code: 404}}) do - body = Poison.decode!(body) - - [] - |> set_success(body) - |> set_params(body) - |> handle_opts() - end - - defp set_success(opts, %{"error" => error}) do - opts ++ [message: error, success: false] - end - - defp set_success(opts, %{"transaction" => %{"response_code" => 20_000}}) do - opts ++ [success: true] - end - - defp parse_body(%{"data" => data}) do - [] - |> set_success(data) - |> parse_authorization(data) - |> parse_status_code(data) - |> set_params(data) - |> handle_opts() - end - - defp handle_opts(opts) do - case Keyword.fetch(opts, :success) do - {:ok, true} -> {:ok, Response.success(opts)} - {:ok, false} -> {:error, Response.error(opts)} - end - end - - # Status code - defp parse_status_code(opts, %{"status" => "failed"} = body) do - response_code = get_in(body, ["transaction", "response_code"]) - response_msg = Map.get(@response_code, response_code, -1) - opts ++ [message: response_msg] - end - - defp parse_status_code(opts, %{"transaction" => transaction}) do - response_code = Map.get(transaction, "response_code", -1) - response_msg = Map.get(@response_code, response_code, -1) - opts ++ [status_code: response_code, message: response_msg] - end - - defp parse_status_code(opts, %{"response_code" => code}) do - response_msg = Map.get(@response_code, code, -1) - opts ++ [status_code: code, message: response_msg] - end - - # Authorization - defp parse_authorization(opts, %{"status" => "failed"}) do - opts ++ [success: false] - end - - defp parse_authorization(opts, %{"id" => id} = auth) do - opts ++ [authorization: id] - end - - defp set_params(opts, body), do: opts ++ [params: body] - end + defp base_url(opts), do: opts[:config][:test_url] || @base_url end diff --git a/mix.exs b/mix.exs index 2a4f2266..589f379c 100644 --- a/mix.exs +++ b/mix.exs @@ -16,11 +16,16 @@ defmodule Gringotts.Mixfile do test_coverage: [ tool: ExCoveralls ], + elixirc_paths: elixirc_paths(Mix.env()), preferred_cli_env: [ coveralls: :test, "coveralls.detail": :test, "coveralls.json": :test, - "coveralls.html": :test + "coveralls.html": :test, + vcr: :test, + "vcr.delete": :test, + "vcr.check": :test, + "vcr.show": :test ], deps: deps(), docs: docs() @@ -36,6 +41,10 @@ defmodule Gringotts.Mixfile do ] end + # Specifies which paths to compile per environment. + defp elixirc_paths(:test), do: ["lib", "test/mocks"] + defp elixirc_paths(_), do: ["lib"] + # Dependencies can be hex.pm packages: # # {:mydep, "~> 0.3.0"} @@ -67,7 +76,8 @@ defmodule Gringotts.Mixfile do {:credo, "~> 0.3", only: [:dev, :test]}, {:inch_ex, "~> 0.5", only: :docs}, {:dialyxir, "~> 0.3", only: :dev}, - {:timex, "~> 3.2"} + {:timex, "~> 3.2"}, + {:exvcr, "~> 0.10", only: :test} ] end diff --git a/mix.lock b/mix.lock index aaaa5cc1..83a82360 100644 --- a/mix.lock +++ b/mix.lock @@ -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"}, diff --git a/test/gateways/authorize_net_test.exs b/test/gateways/authorize_net_test.exs index cec0b0cd..66ae689d 100644 --- a/test/gateways/authorize_net_test.exs +++ b/test/gateways/authorize_net_test.exs @@ -1,5 +1,4 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do - Code.require_file("../mocks/authorize_net_mock.exs", __DIR__) use ExUnit.Case, async: false alias Gringotts.Gateways.AuthorizeNetMock, as: MockResponse alias Gringotts.CreditCard diff --git a/test/gateways/cams_test.exs b/test/gateways/cams_test.exs index a6117a78..43370865 100644 --- a/test/gateways/cams_test.exs +++ b/test/gateways/cams_test.exs @@ -1,5 +1,4 @@ defmodule Gringotts.Gateways.CamsTest do - Code.require_file("../mocks/cams_mock.exs", __DIR__) use ExUnit.Case, async: false alias Gringotts.{ @@ -103,13 +102,19 @@ defmodule Gringotts.Gateways.CamsTest do describe "capture" do test "with full amount" do - with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_capture() end do + with_mock HTTPoison, + post: fn _url, _body, _headers -> + MockResponse.successful_capture() + end do assert {:ok, %Response{}} = Gateway.capture(@money, @id, @options) end end test "with partial amount" do - with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_capture() end do + with_mock HTTPoison, + post: fn _url, _body, _headers -> + MockResponse.successful_capture() + end do assert {:ok, %Response{}} = Gateway.capture(@money_less, @id, @options) end end diff --git a/test/gateways/global_collect_test.exs b/test/gateways/global_collect_test.exs index 8d09449c..ea0f4742 100644 --- a/test/gateways/global_collect_test.exs +++ b/test/gateways/global_collect_test.exs @@ -1,5 +1,4 @@ defmodule Gringotts.Gateways.GlobalCollectTest do - Code.require_file("../mocks/global_collect_mock.exs", __DIR__) use ExUnit.Case, async: false alias Gringotts.Gateways.GlobalCollectMock, as: MockResponse alias Gringotts.Gateways.GlobalCollect diff --git a/test/gateways/paymill_test.exs b/test/gateways/paymill_test.exs new file mode 100644 index 00000000..d830621c --- /dev/null +++ b/test/gateways/paymill_test.exs @@ -0,0 +1,215 @@ +defmodule Gringotts.Gateways.PaymillTest do + use ExUnit.Case, async: true + + alias Gringotts.Gateways.Paymill, as: Gateway + alias Gringotts.Gateways.PaymillMock, as: Mock + alias Plug.{Conn, Parsers} + + setup do + bypass = Bypass.open() + + opts = %{ + private_key: "merchant_secret_key", + public_key: "merchant_public_key", + test_url: "http://localhost:#{bypass.port}/" + } + + {:ok, bypass: bypass, opts: opts} + end + + @amount_42 Money.new(42, :EUR) + @valid_token "tok_d26e611c47d64693a281e8411934" + @invalid_token "tok_d26e611c47d64693a281e841193" + + @transaction_id "tran_de77d38b85d6eee2984accc8b2cc" + @invalid_transaction_id "tran_023d3b5769321c649435" + @capture_preauth_id "preauth_d654694c8116109af903" + @void_id "preauth_0bfc975c2858980a6023" + + describe "authorize" do + test "when token is valid", %{bypass: bypass, opts: opts} do + Bypass.expect(bypass, "POST", "/preauthorizations", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "4200" + assert params["currency"] == "EUR" + assert params["token"] == "tok_d26e611c47d64693a281e8411934" + Conn.resp(conn, 200, Mock.auth_success()) + end) + + {:ok, response} = Gateway.authorize(@amount_42, @valid_token, config: opts) + assert response.gateway_code == 20000 + end + + test "when paymill is down or unreachable", %{bypass: bypass, opts: opts} do + Bypass.down(bypass) + {:error, response} = Gateway.authorize(@amount_42, @valid_token, config: opts) + assert response.reason == "network related failure" + Bypass.up(bypass) + end + + test "when token is invalid", %{bypass: bypass, opts: opts} do + Bypass.expect(bypass, "POST", "/preauthorizations", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "4200" + assert params["currency"] == "EUR" + assert params["token"] == "tok_d26e611c47d64693a281e841193" + Conn.resp(conn, 400, Mock.auth_purchase_invalid_token()) + end) + + {:error, response} = Gateway.authorize(@amount_42, @invalid_token, config: opts) + assert response.status_code == 400 + end + end + + describe "capture" do + test "when preauthorization is valid", %{bypass: bypass, opts: opts} do + Bypass.expect(bypass, "POST", "/transactions", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "4200" + assert params["currency"] == "EUR" + assert params["preauthorization"] == "preauth_d654694c8116109af903" + Conn.resp(conn, 200, Mock.capture_success()) + end) + + {:ok, response} = Gateway.capture(@capture_preauth_id, @amount_42, config: opts) + assert response.gateway_code == 20000 + end + + test "when preauthorization not found", %{bypass: bypass, opts: opts} do + Bypass.expect(bypass, "POST", "/transactions", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "4200" + assert params["currency"] == "EUR" + assert params["preauthorization"] == "preauth_d654694c8116109af903" + Conn.resp(conn, 200, Mock.bad_preauth()) + end) + + {:error, response} = Gateway.capture(@capture_preauth_id, @amount_42, config: opts) + assert response.status_code == 200 + assert response.reason == "Preauthorize not found" + end + + test "when preauthorization done before", %{bypass: bypass, opts: opts} do + Bypass.expect(bypass, "POST", "/transactions", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "4200" + assert params["currency"] == "EUR" + assert params["preauthorization"] == "preauth_d654694c8116109af903" + Conn.resp(conn, 200, Mock.capture_preauth_done_before()) + end) + + {:error, response} = Gateway.capture(@capture_preauth_id, @amount_42, config: opts) + assert response.status_code == 200 + assert response.reason == "Preauthorization has already been used" + end + end + + describe "purchase" do + test "when token is valid", %{bypass: bypass, opts: opts} do + Bypass.expect(bypass, "POST", "/transactions", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "4200" + assert params["currency"] == "EUR" + assert params["token"] == "tok_d26e611c47d64693a281e841193" + Conn.resp(conn, 200, Mock.purchase_valid_token()) + end) + + {:ok, response} = Gateway.purchase(@amount_42, @invalid_token, config: opts) + assert response.gateway_code == 20000 + assert response.fraud_review == true + assert response.status_code == 200 + end + + test "when token is invalid", %{bypass: bypass, opts: opts} do + Bypass.expect(bypass, "POST", "/transactions", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "4200" + assert params["currency"] == "EUR" + assert params["token"] == "tok_d26e611c47d64693a281e841193" + Conn.resp(conn, 200, Mock.auth_purchase_invalid_token()) + end) + + {:error, response} = Gateway.purchase(@amount_42, @invalid_token, config: opts) + assert response.reason["field"] == "token" + + assert response.reason["messages"]["regexNotMatch"] == + "'tok_d26e611c47d64693a281e841193' does not match against pattern '\/^[a-zA-Z0-9_]{32}$\/'" + + assert response.status_code == 200 + end + end + + describe "refund" do + test "when transaction is valid", %{bypass: bypass, opts: opts} do + Bypass.expect(bypass, "POST", "/refunds/#{@transaction_id}", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "4200" + Conn.resp(conn, 200, Mock.refund_success()) + end) + + {:ok, response} = Gateway.refund(@amount_42, @transaction_id, config: opts) + assert response.gateway_code == 20000 + assert response.status_code == 200 + end + + test "when transaction is used again", %{bypass: bypass, opts: opts} do + Bypass.expect(bypass, "POST", "/refunds/#{@transaction_id}", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "4200" + Conn.resp(conn, 200, Mock.refund_again()) + end) + + {:error, response} = Gateway.refund(@amount_42, @transaction_id, config: opts) + assert response.reason == "Amount to high" + assert response.status_code == 200 + end + + test "when transaction not found", %{bypass: bypass, opts: opts} do + Bypass.expect(bypass, "POST", "/refunds/#{@invalid_transaction_id}", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "4200" + Conn.resp(conn, 200, Mock.refund_bad_transaction()) + end) + + {:error, response} = Gateway.refund(@amount_42, @invalid_transaction_id, config: opts) + assert response.reason == "Transaction not found" + assert response.status_code == 200 + end + end + + describe "void" do + test "when preauthorization is valid", %{bypass: bypass, opts: opts} do + Bypass.expect(bypass, "DELETE", "/preauthorizations/#{@void_id}", fn conn -> + Conn.resp(conn, 200, Mock.void_success()) + end) + + {:ok, response} = Gateway.void(@void_id, config: opts) + assert response.gateway_code == 50810 + end + + test "when preauthorization used before", %{bypass: bypass, opts: opts} do + Bypass.expect(bypass, "DELETE", "/preauthorizations/#{@void_id}", fn conn -> + Conn.resp(conn, 200, Mock.void_done_before()) + end) + + {:error, response} = Gateway.void(@void_id, config: opts) + assert response.reason == "Preauthorization was not found" + assert response.status_code == 200 + end + end + + def parse(conn, opts \\ []) do + opts = Keyword.put_new(opts, :parsers, [Parsers.URLENCODED]) + Parsers.call(conn, Parsers.init(opts)) + end +end diff --git a/test/gateways/trexle_test.exs b/test/gateways/trexle_test.exs index 00da54ca..df74ff7d 100644 --- a/test/gateways/trexle_test.exs +++ b/test/gateways/trexle_test.exs @@ -1,5 +1,4 @@ defmodule Gringotts.Gateways.TrexleTest do - Code.require_file("../mocks/trexle_mock.exs", __DIR__) use ExUnit.Case, async: false alias Gringotts.Gateways.TrexleMock, as: MockResponse alias Gringotts.Gateways.Trexle diff --git a/test/gateways/wire_card_test.exs b/test/gateways/wire_card_test.exs index 59de0f03..61633735 100644 --- a/test/gateways/wire_card_test.exs +++ b/test/gateways/wire_card_test.exs @@ -8,8 +8,6 @@ defmodule Gringotts.Gateways.WireCardTest do # TEST_PURCHASE_GUWID = 'C865402121385575982910' # TEST_CAPTURE_GUWID = 'C833707121385268439116' - # credit_card = %CreditCard{name: "Longbob", number: "4200000000000000", cvc: "123", expiration: {2015, 11}} - # config = %{credentails: {'user', 'pass'}, default_currency: "EUR"} :ok end diff --git a/test/integration/gateways/paymill_test.exs b/test/integration/gateways/paymill_test.exs new file mode 100644 index 00000000..f7919ac9 --- /dev/null +++ b/test/integration/gateways/paymill_test.exs @@ -0,0 +1,85 @@ +defmodule Gringotts.Integration.Gateways.PaymillTest do + use ExUnit.Case, async: true + use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney + + alias Gringotts.Gateways.Paymill, as: Gateway + + @moduletag integration: true + + @amount Money.new(4200, :EUR) + @valid_token1 "tok_784c33eeb9a6adfc2bd3c21f95e6" + @valid_token2 "tok_9e429fb2dc44bcf94bcd4e6e6ec5" + @valid_token3 "tok_55b80f87f44f9328bee99360c4cc" + @valid_token4 "tok_7fb13046921783327aaf3f69668c" + @valid_token5 "tok_182291df812e8de23ee7cd849768" + + setup_all do + Application.put_env( + :gringotts, + Gateway, + private_key: "a1bf5c1751ded07471ef246a29709c72", + public_key: "61296669594ebbcc7794acafa9811c4d", + mode: :test + ) + + on_exit(fn -> + Application.delete_env(:gringotts, Gateway) + end) + end + + describe "authorize" do + test "with valid token and currency" do + use_cassette "paymill/authorize with valid token and currency" do + {:ok, response} = Gringotts.authorize(Gateway, @amount, @valid_token1) + assert response.gateway_code == 20000 + assert response.status_code == 200 + end + end + end + + describe "capture" do + test "with valid token currency" do + use_cassette "paymill/capture with valid token currency" do + {:ok, response} = Gringotts.authorize(Gateway, @amount, @valid_token2) + payment_id = response.id + {:ok, response_cap} = Gringotts.capture(Gateway, payment_id, @amount) + assert response_cap.gateway_code == 20000 + assert response_cap.status_code == 200 + end + end + end + + describe "purchase" do + test "with valid token currency" do + use_cassette "paymill purchase with valid token currency" do + {:ok, response} = Gringotts.purchase(Gateway, @amount, @valid_token3) + assert response.gateway_code == 20000 + assert response.status_code == 200 + end + end + end + + describe "refund" do + test "with valid token currency" do + use_cassette "paymill/refund with valid token currency" do + {:ok, response} = Gringotts.purchase(Gateway, @amount, @valid_token4) + trans_id = response.id + {:ok, response_ref} = Gringotts.refund(Gateway, @amount, trans_id) + assert response_ref.gateway_code == 20000 + assert response_ref.status_code == 200 + end + end + end + + describe "void" do + test "with valid token currency" do + use_cassette "paymill/void with valid token currency" do + {:ok, response} = Gringotts.authorize(Gateway, @amount, @valid_token5) + auth_id = response.id + {:ok, response_void} = Gringotts.void(Gateway, auth_id) + assert response_void.gateway_code == 50810 + assert response_void.status_code == 200 + end + end + end +end diff --git a/test/mocks/authorize_net_mock.exs b/test/mocks/authorize_net_mock.ex similarity index 99% rename from test/mocks/authorize_net_mock.exs rename to test/mocks/authorize_net_mock.ex index 712602cd..953a57f5 100644 --- a/test/mocks/authorize_net_mock.exs +++ b/test/mocks/authorize_net_mock.ex @@ -1,4 +1,5 @@ defmodule Gringotts.Gateways.AuthorizeNetMock do + @moduledoc false # purchase mock response def successful_purchase_response do {:ok, diff --git a/test/mocks/cams_mock.exs b/test/mocks/cams_mock.ex similarity index 99% rename from test/mocks/cams_mock.exs rename to test/mocks/cams_mock.ex index 1eb50faf..1698d43c 100644 --- a/test/mocks/cams_mock.exs +++ b/test/mocks/cams_mock.ex @@ -1,4 +1,5 @@ defmodule Gringotts.Gateways.CamsMock do + @moduledoc false def successful_purchase do {:ok, %HTTPoison.Response{ diff --git a/test/mocks/global_collect_mock.exs b/test/mocks/global_collect_mock.ex similarity index 99% rename from test/mocks/global_collect_mock.exs rename to test/mocks/global_collect_mock.ex index ebedf90a..22452930 100644 --- a/test/mocks/global_collect_mock.exs +++ b/test/mocks/global_collect_mock.ex @@ -1,4 +1,6 @@ defmodule Gringotts.Gateways.GlobalCollectMock do + @moduledoc false + def test_for_purchase_with_valid_card do {:ok, %HTTPoison.Response{ diff --git a/test/mocks/paymill_mock.ex b/test/mocks/paymill_mock.ex new file mode 100644 index 00000000..da584905 --- /dev/null +++ b/test/mocks/paymill_mock.ex @@ -0,0 +1,136 @@ +defmodule Gringotts.Gateways.PaymillMock do + @moduledoc false + + def auth_success do + ~s/{ "data":{ "id":"preauth_7f0a5b2787d0acb96db5", "amount":"4200", + "currency":"EUR", "description":"description example", "status":"closed", + "livemode":false, "created_at":1523890381, "updated_at":1523890383, + "app_id":null, "payment":{ "id":"pay_abdd833557398641e9dfcc47", + "type":"creditcard", "client":"client_d8b9c9a37b0ecb1bbd83", + "card_type":"mastercard", "country":"DE", "expire_month":"12", + "expire_year":"2018", "card_holder":"Harry Potter", "last4":"0004", + "updated_at":1522922164, "created_at":1522922164, "app_id":null, + "is_recurring":true, "is_usable_for_preauthorization":true }, "client":{ + "id":"client_d8b9c9a37b0ecb1bbd83", "email":null, "description":null, + "app_id":null, "updated_at":1522922164, "created_at":1522922164, "payment":[ + "pay_abdd833557398641e9dfcc47" ], "subscription":null }, "transaction":{ + "id":"tran_7341c475993e3ddbbff801c47597", "amount":4200, + "origin_amount":4200, "status":"preauth", "description":"description + example", "livemode":false, "refunds":null, + "client":"client_d8b9c9a37b0ecb1bbd83", "currency":"EUR", + "created_at":1523890381, "updated_at":1523890383, "response_code":20000, + "short_id":null, "is_fraud":false, "invoices":[ ], "app_id":null, + "preauthorization":"preauth_7f0a5b2787d0acb96db5", "fees":[ ], + "payment":"pay_abdd833557398641e9dfcc47", "mandate_reference":null, + "is_refundable":false, "is_markable_as_fraud":true } }, "mode":"test" }/ + end + + def auth_purchase_invalid_token do + ~s/{ "error":{ "messages":{ + "regexNotMatch":"'tok_d26e611c47d64693a281e841193' does not match against pattern '\/^[a-zA-Z0-9_]{32}$\/'" + }, "field":"token" } }/ + end + + def purchase_valid_token do + ~s/{ "data":{ "id":"tran_de77d38b85d6eee2984accc8b2cc", "amount":4200, + "origin_amount":4200, "status":"closed", "description":"", "livemode":false, + "refunds":null, "client":{ "id":"client_d8b9c9a37b0ecb1bbd83", "email":null, + "description":null, "app_id":null, "updated_at":1522922164, + "created_at":1522922164, "payment":[ "pay_abdd833557398641e9dfcc47" ], + "subscription":null }, "currency":"EUR", "created_at":1524135111, + "updated_at":1524135111, "response_code":20000, "short_id":"0000.9999.0000", + "is_fraud":false, "invoices":[ ], "app_id":null, "preauthorization":null, + "fees":[ ], "payment":{ "id":"pay_abdd833557398641e9dfcc47", + "type":"creditcard", "client":"client_d8b9c9a37b0ecb1bbd83", + "card_type":"mastercard", "country":"DE", "expire_month":"12", + "expire_year":"2018", "card_holder":"Sagar Karwande", "last4":"0004", + "updated_at":1522922164, "created_at":1522922164, "app_id":null, + "is_recurring":true, "is_usable_for_preauthorization":true }, + "mandate_reference":null, "is_refundable":true, "is_markable_as_fraud":true + }, "mode":"test" }/ + end + + def refund_success do + ~s/{ "data":{ "id":"refund_96a0c66456a55ba3e746", "amount":4200, + "status":"refunded", "description":null, "livemode":false, + "created_at":1524138133, "updated_at":1524138133, + "short_id":"0000.9999.0000", "response_code":20000, "reason":null, + "app_id":null, "transaction":{ "id":"tran_de77d38b85d6eee2984accc8b2cc", + "amount":0, "origin_amount":4200, "status":"refunded", "description":"", + "livemode":false, "refunds":[ "refund_96a0c66456a55ba3e746" ], + "client":"client_d8b9c9a37b0ecb1bbd83", "currency":"EUR", + "created_at":1524135111, "updated_at":1524138134, "response_code":20000, + "short_id":"0000.9999.0000", "is_fraud":false, "invoices":[ ], + "app_id":null, "preauthorization":null, "fees":[ ], + "payment":"pay_abdd833557398641e9dfcc47", "mandate_reference":null, + "is_refundable":false, "is_markable_as_fraud":true } }, "mode":"test" }/ + end + + def refund_again do + ~s/{ "exception":"refund_amount_to_high", "error":"Amount to high" }/ + end + + def refund_bad_transaction do + ~s/{ "exception":"transaction_not_found", "error":"Transaction not found" }/ + end + + def capture_success do + ~s/{ "data":{ "id":"tran_2f46c44c4d5219e4ef4b7c6292ba", "amount":4200, + "origin_amount":4200, "status":"closed", "description":"", "livemode":false, + "refunds":null, "client":{ "id":"client_d8b9c9a37b0ecb1bbd83", "email":null, + "description":null, "app_id":null, "updated_at":1522922164, + "created_at":1522922164, "payment":[ "pay_abdd833557398641e9dfcc47" ], + "subscription":null }, "currency":"EUR", "created_at":1524138666, + "updated_at":1524138699, "response_code":20000, "short_id":"0000.9999.0000", + "is_fraud":false, "invoices":[ ], "app_id":null, "preauthorization":{ + "id":"preauth_d654694c8116109af903", "amount":"4200", "currency":"EUR", + "description":"description example", "status":"closed", "livemode":false, + "created_at":1524138666, "updated_at":1524138669, "app_id":null, + "payment":"pay_abdd833557398641e9dfcc47", + "client":"client_d8b9c9a37b0ecb1bbd83", + "transaction":"tran_2f46c44c4d5219e4ef4b7c6292ba" }, "fees":[ ], "payment":{ + "id":"pay_abdd833557398641e9dfcc47", "type":"creditcard", + "client":"client_d8b9c9a37b0ecb1bbd83", "card_type":"mastercard", + "country":"DE", "expire_month":"12", "expire_year":"2018", + "card_holder":"Sagar Karwande", "last4":"0004", "updated_at":1522922164, + "created_at":1522922164, "app_id":null, "is_recurring":true, + "is_usable_for_preauthorization":true }, "mandate_reference":null, + "is_refundable":true, "is_markable_as_fraud":true }, "mode":"test" }/ + end + + def bad_preauth do + ~s/{ "exception":"not_found_transaction_preauthorize", "error":"Preauthorize not found" }/ + end + + def capture_preauth_done_before do + ~s/{ "exception":"preauthorization_already_used", "error":"Preauthorization has already been used" }/ + end + + def void_success do + ~s/{ "data":{ "id":"preauth_0bfc975c2858980a6023", + "amount":"4200", "currency":"EUR", "description":"description example", + "status":"deleted", "livemode":false, "created_at":1524140381, + "updated_at":1524140479, "app_id":null, "payment":{ + "id":"pay_abdd833557398641e9dfcc47", "type":"creditcard", + "client":"client_d8b9c9a37b0ecb1bbd83", "card_type":"mastercard", + "country":"DE", "expire_month":"12", "expire_year":"2018", + "card_holder":"Sagar Karwande", "last4":"0004", "updated_at":1522922164, + "created_at":1522922164, "app_id":null, "is_recurring":true, + "is_usable_for_preauthorization":true }, "client":{ + "id":"client_d8b9c9a37b0ecb1bbd83", "email":null, "description":null, + "app_id":null, "updated_at":1522922164, "created_at":1522922164, "payment":[ + "pay_abdd833557398641e9dfcc47" ], "subscription":null }, "transaction":{ + "id":"tran_f360d805dce7f84baf07077a7f96", "amount":4200, "origin_amount":4200, + "status":"failed", "description":"description example", "livemode":false, + "refunds":null, "client":"client_d8b9c9a37b0ecb1bbd83", "currency":"EUR", + "created_at":1524140381, "updated_at":1524140479, "response_code":50810, + "short_id":null, "is_fraud":false, "invoices":[ ], "app_id":null, + "preauthorization":"preauth_0bfc975c2858980a6023", "fees":[ ], + "payment":"pay_abdd833557398641e9dfcc47", "mandate_reference":null, + "is_refundable":false, "is_markable_as_fraud":true } }, "mode":"test" }/ + end + + def void_done_before do + ~s/{ "exception":"preauthorization_not_found", "error":"Preauthorization was not found" }/ + end +end diff --git a/test/mocks/trexle_mock.exs b/test/mocks/trexle_mock.ex similarity index 93% rename from test/mocks/trexle_mock.exs rename to test/mocks/trexle_mock.ex index 73f5aa9e..a0229b24 100644 --- a/test/mocks/trexle_mock.exs +++ b/test/mocks/trexle_mock.ex @@ -1,4 +1,6 @@ defmodule Gringotts.Gateways.TrexleMock do + @moduledoc false + def test_for_purchase_with_valid_card do {:ok, %HTTPoison.Response{ @@ -194,8 +196,13 @@ defmodule Gringotts.Gateways.TrexleMock do def test_for_store_with_valid_card do {:ok, %HTTPoison.Response{ - body: - ~s/{"response":{"token":"token_94e333959850270460e89a86bad2246613528cbb","card":{"token":"token_2a1ba29522e4a377fafa62e8e204f76ad8ba8f1e","scheme":"master","display_number":"XXXX-XXXX-XXXX-8210","expiry_year":2018,"expiry_month":1,"cvc":123,"name":"John Doe","address_line1":"456 My Street","address_line2":null,"address_city":"Ottawa","address_state":"ON","address_postcode":"K1C2N6","address_country":"CA","primary":true}}}/, + body: ~s/{"response":{"token":"token_94e333959850270460e89a86bad2246613528cbb", + "card":{"token":"token_2a1ba29522e4a377fafa62e8e204f76ad8ba8f1e", + "scheme":"master","display_number":"XXXX-XXXX-XXXX-8210","expiry_year":2018,"expiry_month":1, + "cvc":123,"name":"John Doe","address_line1":"456 My Street", + "address_line2":null, "address_city":"Ottawa", + "address_state":"ON","address_postcode":"K1C2N6", + "address_country":"CA","primary":true}}}/, headers: [ {"Date", "Sat, 23 Dec 2017 19:32:58 GMT"}, {"Content-Type", "application/json; charset=UTF-8"}, From 21c55172c8e08b1cd6268d81c9ec65196b6435ca Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Wed, 25 Apr 2018 19:12:06 +0530 Subject: [PATCH 19/24] Add clause to catch/handle bad config format (#151) * Add clause to catch/handle bad config format Closes #150 Closes #136 * [ci-skip] Improve mock test template * Since all mock tests are to be written using Bypass, removed mock file as well as mention of `mock` library. * Added setup and parse to the test template --- README.md | 7 ++-- lib/gringotts.ex | 1 - lib/gringotts/adapter.ex | 8 ++++- templates/gateway.eex | 4 +-- templates/integration.eex | 10 ++++-- templates/test.eex | 68 ++++++++++++++++++++++++++++----------- 6 files changed, 70 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index ce98c205..8f4f4a82 100644 --- a/README.md +++ b/README.md @@ -122,8 +122,11 @@ We wanted to "supervise" our payments, and power utilities to process recurring payments, subscriptions with it. But yes, as of now, it is a bottle neck and unnecessary. -It's slated to be removed in [`v2.0.0`][milestone-2_0_0_alpha] and any supervised / async / -parallel work can be explicitly managed via native elixir constructs. +It's slated to be removed in [`v2.0.0`][milestone-2_0_0_alpha] and any +supervised/async/parallel work can be explicitly managed via native elixir +constructs. + +**In fact, it's already been removed from our [dev](#) branch.** [milestone-2_0_0_alpha]: https://github.com/aviabird/gringotts/milestone/3 [reason]: http://harrypotter.wikia.com/wiki/Gringotts diff --git a/lib/gringotts.ex b/lib/gringotts.ex index 003c6aba..161d1ae8 100644 --- a/lib/gringotts.ex +++ b/lib/gringotts.ex @@ -295,7 +295,6 @@ defmodule Gringotts do end defp get_and_validate_config(gateway) do - # Keep the key name and adapter the same in the config in application config = Application.get_env(:gringotts, gateway) # The following call to validate_config might raise an error gateway.validate_config(config) diff --git a/lib/gringotts/adapter.ex b/lib/gringotts/adapter.ex index 5edd06f2..f4380b87 100644 --- a/lib/gringotts/adapter.ex +++ b/lib/gringotts/adapter.ex @@ -49,7 +49,7 @@ defmodule Gringotts.Adapter do Raises a run-time `ArgumentError` if any of the `required_config` values is not available or missing from the Application config. """ - def validate_config(config) do + def validate_config(config) when is_list(config) do missing_keys = Enum.reduce(@required_config, [], fn key, missing_keys -> if config[key] in [nil, ""], do: [key | missing_keys], else: missing_keys @@ -58,6 +58,12 @@ defmodule Gringotts.Adapter do raise_on_missing_config(missing_keys, config) end + def validate_config(config) when is_map(config) do + config + |> Enum.into([]) + |> validate_config + end + defp raise_on_missing_config([], _config), do: :ok defp raise_on_missing_config(key, config) do diff --git a/templates/gateway.eex b/templates/gateway.eex index 6169bae4..48de7269 100644 --- a/templates/gateway.eex +++ b/templates/gateway.eex @@ -65,7 +65,7 @@ defmodule Gringotts.Gateways.<%= gateway_module %> do ## Following the examples - 1. First, set up a sample application and configure it to work with MONEI. + 1. First, set up a sample application and configure it to work with <%= gateway %>. - 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. @@ -258,7 +258,6 @@ defmodule Gringotts.Gateways.<%= gateway_module %> do # 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(_) do # resp = HTTPoison.request(args, ...) # respond(resp, ...) @@ -266,7 +265,6 @@ defmodule Gringotts.Gateways.<%= gateway_module %> do # Parses <%= gateway %>'s response and returns a `Gringotts.Response` struct # in a `:ok`, `:error` tuple. - @spec respond(term) :: {:ok | :error, Response} defp respond(<%= gateway_underscore %>_response) defp respond({:ok, %{status_code: 200, body: body}}), do: "something" defp respond({:ok, %{status_code: status_code, body: body}}), do: "something" diff --git a/templates/integration.eex b/templates/integration.eex index 74bc5fc7..46e5b063 100644 --- a/templates/integration.eex +++ b/templates/integration.eex @@ -1,7 +1,10 @@ -defmodule Gringotts.Integration.Gateways.<%= gateway_module <> "Test"%> do - # Integration tests for the <%= gateway_module%> +defmodule Gringotts.Integration.Gateways.<%= gateway_module <> "Test" %> do + # Integration tests for the <%= gateway_module %> + # + # Note that your tests SHOULD NOT directly call the <%= gateway_module %>, but + # all calls must be via Gringotts' public API as defined in `lib`gringotts.ex` - use ExUnit.Case, async: false + use ExUnit.Case, async: true alias Gringotts.Gateways.<%= gateway_module%> @moduletag :integration @@ -15,6 +18,7 @@ defmodule Gringotts.Integration.Gateways.<%= gateway_module <> "Test"%> do end # Group the test cases by public api + describe "purchase" do end diff --git a/templates/test.eex b/templates/test.eex index 93c0b62d..f91b8d47 100644 --- a/templates/test.eex +++ b/templates/test.eex @@ -1,32 +1,64 @@ defmodule Gringotts.Gateways.<%= gateway_module <> "Test" %> do - # The file contains mocked tests for <%= gateway_module%> + # The file contains mock tests for <%= gateway_module%> - # We recommend using [mock][1] for this, you can place the mock responses from - # the Gateway in `test/mocks/<%= mock_response_filename %>` file, which has also been - # generated for you. - # - # [1]: https://github.com/jjh42/mock + # We recommend using [`Bypass`][bypass] for this as it allows us to inspect + # the request body that is sent to the gateway. + + # After all, the only thing Gringotts does, is building HTTPoison requests + # from arguments. Thus by validating that a request has been properly + # constructed from the given arguments we accurately cover the behaviour of + # the module. + + # For inspiration and guidance to writing mock tests, please refer the mock + # tests of the MONEI gateway. Bypass has excellent documentation and there are + # numerous blog posts detailing good practices. - # Load the mock response file before running the tests. - Code.require_file "../mocks/<%= mock_response_filename %>", __DIR__ + # [bypass]: https://github.com/pspdfkit-labs/bypass - use ExUnit.Case, async: false + use ExUnit.Case, async: true + + import Bypass + alias Gringotts.Gateways.<%= gateway_module%> - import Mock + alias Plug.{Conn, Parsers} - # Group the test cases by public api - describe "purchase" do + # A new Bypass instance is needed per test, so that we can do parallel tests + setup do + bypass = Bypass.open() + {:ok, bypass: bypass} end - describe "authorize" do - end + @doc """ + Parses the body of the `Plug.Conn.t`. - describe "capture" do - end + This is very useful when testing with `Bypass` to parse body of the request + built in the test. This makes it dead-simple to write asserts on the request + body! + + ## Example + ``` + test "something", %{bypass: bypass} do + Bypass.expect(bypass, "POST", "some/endpoint/", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "42.00" + assert params["currency"] == "USD" + Conn.resp(conn, 200, "the_mocked_reponse_body") + end) - describe "void" do + {:ok, response} = Gateway.authorize(@amount42, @card, @opts) + assert "something about the mocked response if necessary" end + ``` + """ + @spec parse(Plug.Conn.t(), keyword) :: Plug.Conn.t() + def parse(conn, opts \\ []) do + opts = Keyword.put_new(opts, :parsers, [Parsers.URLENCODED]) - describe "refund" do + # if your gateway returns JSON instead of URL Encoded responses, use the + # JSON parser + + # opts = Keyword.put_new(opts, :parsers, [Parsers.JSON]) + Parsers.call(conn, Parsers.init(opts)) end end From ce714be0a97a7f731a284da2e448811032d75cd6 Mon Sep 17 00:00:00 2001 From: ravirocx Date: Tue, 1 May 2018 02:42:56 +0530 Subject: [PATCH 20/24] Implementation of all functions and its test cases --- lib/gringotts/gateways/checkout.ex | 382 ++++++++++++++++++++ test/integration/gateways/checkout_test.exs | 140 +++++++ 2 files changed, 522 insertions(+) create mode 100644 lib/gringotts/gateways/checkout.ex create mode 100644 test/integration/gateways/checkout_test.exs diff --git a/lib/gringotts/gateways/checkout.ex b/lib/gringotts/gateways/checkout.ex new file mode 100644 index 00000000..89ec5013 --- /dev/null +++ b/lib/gringotts/gateways/checkout.ex @@ -0,0 +1,382 @@ +defmodule Gringotts.Gateways.Checkout do + @moduledoc """ + [checkout][home] gateway implementation. + + ## Instructions! + + ***This is an example `moduledoc`, and suggests some items that should be + documented in here.*** + + The quotation boxes like the one below will guide you in writing excellent + documentation for your gateway. All our gateways are documented in this manner + and we aim to keep our docs as consistent with each other as possible. + **Please read them and do as they suggest**. Feel free to add or skip sections + though. + + If you'd like to make edits to the template docs, they exist at + `templates/gateway.eex`. We encourage you to make corrections and open a PR + and tag it with the label `template`. + + ***Actual docs begin below this line!*** + + -------------------------------------------------------------------------------- + + > List features that have been implemented, and what "actions" they map to as + > per the checkout gateway docs. + > A table suits really well for this. + + ## Optional or extra parameters + + Most `Gringotts` API calls accept an optional `Keyword` list `opts` to supply + optional arguments for transactions with the gateway. + + > List all available (ie, those that will be supported by this module) keys, a + > description of their function/role and whether they have been implemented + > and tested. + > A table suits really well for this. + + ## Registering your checkout account at `Gringotts` + + Explain how to make an account with the gateway and show how to put the + `required_keys` (like authentication info) to the configuration. + + > Here's how the secrets map to the required configuration parameters for checkout: + > + > | Config parameter | checkout secret | + > | ------- | ---- | + > | `:secret_key` | **SecretKey** | + + > Your Application config **must include the `[:secret_key]` field(s)** and would look + > something like this: + > + > config :gringotts, Gringotts.Gateways.Checkout, + > secret_key: "your_secret_secret_key" + + ## Scope of this module + + > It's unlikely that your first iteration will support all features of the + > gateway, so list down those items that are missing. + + ## Supported currencies and countries + + > It's enough if you just add a link to the gateway's docs or FAQ that provide + > info about this. + + ## Following the examples + + 1. First, set up a sample application and configure it to work with checkout. + - 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-monei-account-at-checkout). + + 2. Run an `iex` session with `iex -S mix` and add some variable bindings and + aliases to it (to save some time): + ``` + iex> alias Gringotts.{Response, CreditCard, Gateways.Checkout} + iex> card = %CreditCard{first_name: "Jo", + last_name: "Doe", + number: "4200000000000000", + year: 2099, month: 12, + verification_code: "123", brand: "VISA"} + ``` + + > Add any other frequently used bindings up here. + + We'll be using these in the examples below. + + [gs]: https://github.com/aviabird/gringotts/wiki/ + [home]: https://www.checkout.com + [example]: https://github.com/aviabird/gringotts_example + """ + + # The Base module has the (abstract) public API, and some utility + # implementations. + 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: [:secret_key] + + import Poison, only: [decode: 1] + + alias Gringotts.{Money, CreditCard, Response} + + @test_url "https://sandbox.checkout.com/api2/v2/" + @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. + + > ** You could perhaps:** + > 1. describe what are the important fields in the Response struct + > 2. mention what a merchant can do with these important fields (ex: + > `capture/3`, etc.) + + ## Note + + > If there's anything noteworthy about this operation, it comes here. + + ## Example + + > A barebones example using the bindings you've suggested in the `moduledoc`. + """ + @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} + def authorize(amount, card = %CreditCard{}, opts) do + {currency, value, _} = Money.to_integer(amount) + + body = + Poison.encode!(%{ + email: opts[:email], + currency: currency, + value: value, + autoCapture: "n", + autoCapTime: opts[:autoCapTime], + shippingDetails: %{ + addressLine1: opts[:address].street1, + addressLine2: opts[:address].street2, + city: opts[:address].city, + state: opts[:address].region, + country: opts[:address].country, + postcode: opts[:address].postal_code, + phone: %{ + countryCode: opts[:countryCode], + number: opts[:number] + } + }, + chargeMode: opts[:chargeMode], + customerIp: opts[:customerIp], + customerName: opts[:customerName], + description: opts[:description], + descriptor: opts[:descriptor], + trackId: opts[:trackId], + card: %{ + number: card.number, + name: CreditCard.full_name(card), + cvv: card.verification_code, + expiryMonth: card.month, + expiryYear: card.year, + billingDetails: %{ + addressLine1: opts[:address].street1, + addressLine2: opts[:address].street2, + city: opts[:address].city, + state: opts[:address].region, + country: opts[:address].country, + postcode: opts[:address].postal_code, + phone: %{ + countryCode: opts[:countryCode], + number: opts[:number] + } + } + } + }) + + commit(:post, "charges/card", body, opts) + end + + @doc """ + Captures a pre-authorized `amount`. + + `amount` is transferred to the merchant account by checkout used in the + pre-authorization referenced by `payment_id`. + + ## Note + + > If there's anything noteworthy about this operation, it comes here. + > For example, does the gateway support partial, multiple captures? + + ## Example + + > A barebones example using the bindings you've suggested in the `moduledoc`. + """ + @spec capture(String.t(), Money.t(), keyword) :: {:ok | :error, Response} + def capture(payment_id, amount, opts) do + {currency, value, _} = Money.to_integer(amount) + + body = + Poison.encode!(%{ + description: opts[:description], + trackId: opts[:trackId], + value: value + }) + + commit(:post, "charges/#{payment_id}/capture", body, opts) + end + + @doc """ + Transfers `amount` from the customer to the merchant. + + checkout attempts to process a purchase on behalf of the customer, by + debiting `amount` from the customer's account by charging the customer's + `card`. + + ## Note + + > If there's anything noteworthy about this operation, it comes here. + + ## Example + + > A barebones example using the bindings you've suggested in the `moduledoc`. + """ + @spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} + def purchase(amount, card = %CreditCard{}, opts) do + {currency, value, _} = Money.to_integer(amount) + + body = + Poison.encode!(%{ + email: opts[:email], + currency: currency, + value: value, + autoCapTime: opts[:autoCapTime], + shippingDetails: %{ + addressLine1: opts[:address].street1, + addressLine2: opts[:address].street2, + city: opts[:address].city, + state: opts[:address].region, + country: opts[:address].country, + postcode: opts[:address].postal_code, + phone: %{ + countryCode: opts[:countryCode], + number: opts[:number] + } + }, + chargeMode: opts[:chargeMode], + customerIp: opts[:customerIp], + customerName: opts[:customerName], + description: opts[:description], + descriptor: opts[:descriptor], + trackId: opts[:trackId], + card: %{ + number: card.number, + name: CreditCard.full_name(card), + cvv: card.verification_code, + expiryMonth: card.month, + expiryYear: card.year, + billingDetails: %{ + addressLine1: opts[:address].street1, + addressLine2: opts[:address].street2, + city: opts[:address].city, + state: opts[:address].region, + country: opts[:address].country, + postcode: opts[:address].postal_code, + phone: %{ + countryCode: opts[:countryCode], + number: opts[:number] + } + } + } + }) + + commit(:post, "charges/card", body, opts) + 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 + + > Which transactions can be voided? + > Is there a limited time window within which a void can be perfomed? + + ## Example + + > A barebones example using the bindings you've suggested in the `moduledoc`. + """ + @spec void(String.t(), keyword) :: {:ok | :error, Response} + def void(payment_id, opts) do + body = + Poison.encode!(%{ + description: opts[:description], + trackId: opts[:trackId] + }) + + commit(:post, "charges/#{payment_id}/void", body, opts) + end + + @doc """ + Refunds the `amount` to the customer's account with reference to a prior transfer. + + > Refunds are allowed on which kinds of "prior" transactions? + + ## Note + + > The end customer will usually see two bookings/records on his statement. Is + > that true for checkout? + > Is there a limited time window within which a void can be perfomed? + + ## Example + + > A barebones example using the bindings you've suggested in the `moduledoc`. + """ + @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response} + def refund(amount, payment_id, opts) do + {currency, value, _} = Money.to_integer(amount) + + body = + Poison.encode!(%{ + description: opts[:description], + trackId: opts[:trackId], + value: value + }) + + commit(:post, "charges/#{payment_id}/refund", body, opts) + end + + ############################################################################### + # PRIVATE METHODS # + ############################################################################### + + # Makes the request to checkout'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) :: {:ok | :error, Response} + defp commit(:post, endpoint, body, opts) do + url = @test_url <> "#{endpoint}" + + headers = [ + {"Content-Type", "application/json;charset=UTF-8"}, + {"Authorization", opts[:config][:secret_key]} + ] + + HTTPoison.request(:post, url, body, headers) + |> respond + end + + # Parses checkout's response and returns a `Gringotts.Response` struct + # in a `:ok`, `:error` tuple. + @spec respond(term) :: {:ok | :error, Response} + defp respond({:ok, %{status_code: code, body: body}}) when code in 200..299 do + {:ok, parsed} = decode(body) + + id = parsed["id"] + message = parsed["status"] + + { + :ok, + Response.success(id: id, message: message, raw: parsed, status_code: code) + } + end + + defp respond({:ok, %{status_code: status_code, body: body}}) do + {:ok, parsed} = decode(body) + detail = parsed["error_description"] + + { + :error, + Response.error(status_code: status_code, message: detail, raw: body) + } + end + + defp respond({:error, %HTTPoison.Error{} = error}) do + {:error, Response.error(code: error.id, message: "HTTPoison says '#{error.reason}")} + end +end diff --git a/test/integration/gateways/checkout_test.exs b/test/integration/gateways/checkout_test.exs new file mode 100644 index 00000000..f6292b74 --- /dev/null +++ b/test/integration/gateways/checkout_test.exs @@ -0,0 +1,140 @@ +defmodule Gringotts.Integration.Gateways.CheckoutTest do + # Integration tests for the Checkout + use ExUnit.Case, async: false + use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney + alias Gringotts.Gateways.Checkout + + alias Gringotts.{ + CreditCard, + Address + } + + alias Gringotts.Gateways.Checkout, as: Gateway + + # @moduletag :integration + + @amount Money.new(420, :USD) + + @bad_card1 %CreditCard{ + first_name: "Harry", + last_name: "Potter", + number: "4100000000000001", + year: 2009, + month: 12, + verification_code: "123", + brand: "VISA" + } + + @good_card %CreditCard{ + number: "4543474002249996", + month: 06, + year: 2025, + first_name: "Harry", + last_name: " Potter", + verification_code: "956", + brand: "VISA" + } + + @add %Address{ + street1: "OBH", + street2: "AIT", + city: "PUNE", + region: "MH", + country: "IN", + postal_code: "411015", + phone: "8007810916" + } + + @opts [ + description: "hello", + email: "hi@hello.com", + ip_address: "1.1.1.1", + chargeMode: 1, + config: [ + secret_key: "sk_test_f3695cf1-4f36-485b-bba9-caa5b5acb028" + ], + address: @add + ] + + describe "authorize" do + test "[authorize] with good parameters" do + use_cassette "Checkout/authorize_with_valid_card" do + assert {:ok, response} = Gateway.authorize(@amount, @good_card, @opts) + assert response.success == true + assert response.status_code == 200 + end + end + + test "[authorize] with bad CreditCard" do + use_cassette "Checkout/authorize_with_invalid_card" do + assert {:error, response} = Gateway.authorize(@amount, @bad_card1, @opts) + assert response.success == false + assert response.status_code == 400 + end + end + end + + describe "purchase" do + test "[purchase] with good parameters" do + use_cassette "Checkout/purchase_with_valid_card" do + assert {:ok, response} = Gateway.purchase(@amount, @good_card, @opts) + assert response.success == true + assert response.status_code == 200 + end + end + + test "[purchase] with bad CreditCard" do + use_cassette "Checkout/purchase_with_invalid_card" do + assert {:error, response} = Gateway.purchase(@amount, @bad_card1, @opts) + assert response.success == false + assert response.status_code == 400 + end + end + end + + describe "capture" do + test "[Capture]" do + use_cassette "Checkout/capture" do + assert {:ok, response} = Gateway.authorize(@amount, @good_card, @opts) + assert response.success == true + assert response.status_code == 200 + payment_id = response.id + assert {:ok, response} = Gateway.capture(payment_id, @amount, @opts) + assert response.success == true + assert response.status_code == 200 + end + end + end + + describe "Void" do + test "[Void]" do + use_cassette "Checkout/void" do + assert {:ok, response} = Gateway.authorize(@amount, @good_card, @opts) + assert response.success == true + assert response.status_code == 200 + payment_id = response.id + assert {:ok, response} = Gateway.void(payment_id, @opts) + assert response.success == true + assert response.status_code == 200 + end + end + end + + describe "Refund" do + test "[Refund]" do + use_cassette "Checkout/Refund" do + assert {:ok, response} = Gateway.authorize(@amount, @good_card, @opts) + assert response.success == true + assert response.status_code == 200 + payment_id = response.id + assert {:ok, response} = Gateway.capture(payment_id, @amount, @opts) + assert response.success == true + assert response.status_code == 200 + payment_id = response.id + assert {:ok, response} = Gateway.refund(@amount, payment_id, @opts) + assert response.success == true + assert response.status_code == 200 + end + end + end +end From 5714928367824961a421e84df64e6e77827c149c Mon Sep 17 00:00:00 2001 From: ravirocx Date: Tue, 1 May 2018 02:46:18 +0530 Subject: [PATCH 21/24] Revert "Implementation of all functions and its test cases" This reverts commit ce714be0a97a7f731a284da2e448811032d75cd6. --- lib/gringotts/gateways/checkout.ex | 382 -------------------- test/integration/gateways/checkout_test.exs | 140 ------- 2 files changed, 522 deletions(-) delete mode 100644 lib/gringotts/gateways/checkout.ex delete mode 100644 test/integration/gateways/checkout_test.exs diff --git a/lib/gringotts/gateways/checkout.ex b/lib/gringotts/gateways/checkout.ex deleted file mode 100644 index 89ec5013..00000000 --- a/lib/gringotts/gateways/checkout.ex +++ /dev/null @@ -1,382 +0,0 @@ -defmodule Gringotts.Gateways.Checkout do - @moduledoc """ - [checkout][home] gateway implementation. - - ## Instructions! - - ***This is an example `moduledoc`, and suggests some items that should be - documented in here.*** - - The quotation boxes like the one below will guide you in writing excellent - documentation for your gateway. All our gateways are documented in this manner - and we aim to keep our docs as consistent with each other as possible. - **Please read them and do as they suggest**. Feel free to add or skip sections - though. - - If you'd like to make edits to the template docs, they exist at - `templates/gateway.eex`. We encourage you to make corrections and open a PR - and tag it with the label `template`. - - ***Actual docs begin below this line!*** - - -------------------------------------------------------------------------------- - - > List features that have been implemented, and what "actions" they map to as - > per the checkout gateway docs. - > A table suits really well for this. - - ## Optional or extra parameters - - Most `Gringotts` API calls accept an optional `Keyword` list `opts` to supply - optional arguments for transactions with the gateway. - - > List all available (ie, those that will be supported by this module) keys, a - > description of their function/role and whether they have been implemented - > and tested. - > A table suits really well for this. - - ## Registering your checkout account at `Gringotts` - - Explain how to make an account with the gateway and show how to put the - `required_keys` (like authentication info) to the configuration. - - > Here's how the secrets map to the required configuration parameters for checkout: - > - > | Config parameter | checkout secret | - > | ------- | ---- | - > | `:secret_key` | **SecretKey** | - - > Your Application config **must include the `[:secret_key]` field(s)** and would look - > something like this: - > - > config :gringotts, Gringotts.Gateways.Checkout, - > secret_key: "your_secret_secret_key" - - ## Scope of this module - - > It's unlikely that your first iteration will support all features of the - > gateway, so list down those items that are missing. - - ## Supported currencies and countries - - > It's enough if you just add a link to the gateway's docs or FAQ that provide - > info about this. - - ## Following the examples - - 1. First, set up a sample application and configure it to work with checkout. - - 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-monei-account-at-checkout). - - 2. Run an `iex` session with `iex -S mix` and add some variable bindings and - aliases to it (to save some time): - ``` - iex> alias Gringotts.{Response, CreditCard, Gateways.Checkout} - iex> card = %CreditCard{first_name: "Jo", - last_name: "Doe", - number: "4200000000000000", - year: 2099, month: 12, - verification_code: "123", brand: "VISA"} - ``` - - > Add any other frequently used bindings up here. - - We'll be using these in the examples below. - - [gs]: https://github.com/aviabird/gringotts/wiki/ - [home]: https://www.checkout.com - [example]: https://github.com/aviabird/gringotts_example - """ - - # The Base module has the (abstract) public API, and some utility - # implementations. - 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: [:secret_key] - - import Poison, only: [decode: 1] - - alias Gringotts.{Money, CreditCard, Response} - - @test_url "https://sandbox.checkout.com/api2/v2/" - @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. - - > ** You could perhaps:** - > 1. describe what are the important fields in the Response struct - > 2. mention what a merchant can do with these important fields (ex: - > `capture/3`, etc.) - - ## Note - - > If there's anything noteworthy about this operation, it comes here. - - ## Example - - > A barebones example using the bindings you've suggested in the `moduledoc`. - """ - @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} - def authorize(amount, card = %CreditCard{}, opts) do - {currency, value, _} = Money.to_integer(amount) - - body = - Poison.encode!(%{ - email: opts[:email], - currency: currency, - value: value, - autoCapture: "n", - autoCapTime: opts[:autoCapTime], - shippingDetails: %{ - addressLine1: opts[:address].street1, - addressLine2: opts[:address].street2, - city: opts[:address].city, - state: opts[:address].region, - country: opts[:address].country, - postcode: opts[:address].postal_code, - phone: %{ - countryCode: opts[:countryCode], - number: opts[:number] - } - }, - chargeMode: opts[:chargeMode], - customerIp: opts[:customerIp], - customerName: opts[:customerName], - description: opts[:description], - descriptor: opts[:descriptor], - trackId: opts[:trackId], - card: %{ - number: card.number, - name: CreditCard.full_name(card), - cvv: card.verification_code, - expiryMonth: card.month, - expiryYear: card.year, - billingDetails: %{ - addressLine1: opts[:address].street1, - addressLine2: opts[:address].street2, - city: opts[:address].city, - state: opts[:address].region, - country: opts[:address].country, - postcode: opts[:address].postal_code, - phone: %{ - countryCode: opts[:countryCode], - number: opts[:number] - } - } - } - }) - - commit(:post, "charges/card", body, opts) - end - - @doc """ - Captures a pre-authorized `amount`. - - `amount` is transferred to the merchant account by checkout used in the - pre-authorization referenced by `payment_id`. - - ## Note - - > If there's anything noteworthy about this operation, it comes here. - > For example, does the gateway support partial, multiple captures? - - ## Example - - > A barebones example using the bindings you've suggested in the `moduledoc`. - """ - @spec capture(String.t(), Money.t(), keyword) :: {:ok | :error, Response} - def capture(payment_id, amount, opts) do - {currency, value, _} = Money.to_integer(amount) - - body = - Poison.encode!(%{ - description: opts[:description], - trackId: opts[:trackId], - value: value - }) - - commit(:post, "charges/#{payment_id}/capture", body, opts) - end - - @doc """ - Transfers `amount` from the customer to the merchant. - - checkout attempts to process a purchase on behalf of the customer, by - debiting `amount` from the customer's account by charging the customer's - `card`. - - ## Note - - > If there's anything noteworthy about this operation, it comes here. - - ## Example - - > A barebones example using the bindings you've suggested in the `moduledoc`. - """ - @spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} - def purchase(amount, card = %CreditCard{}, opts) do - {currency, value, _} = Money.to_integer(amount) - - body = - Poison.encode!(%{ - email: opts[:email], - currency: currency, - value: value, - autoCapTime: opts[:autoCapTime], - shippingDetails: %{ - addressLine1: opts[:address].street1, - addressLine2: opts[:address].street2, - city: opts[:address].city, - state: opts[:address].region, - country: opts[:address].country, - postcode: opts[:address].postal_code, - phone: %{ - countryCode: opts[:countryCode], - number: opts[:number] - } - }, - chargeMode: opts[:chargeMode], - customerIp: opts[:customerIp], - customerName: opts[:customerName], - description: opts[:description], - descriptor: opts[:descriptor], - trackId: opts[:trackId], - card: %{ - number: card.number, - name: CreditCard.full_name(card), - cvv: card.verification_code, - expiryMonth: card.month, - expiryYear: card.year, - billingDetails: %{ - addressLine1: opts[:address].street1, - addressLine2: opts[:address].street2, - city: opts[:address].city, - state: opts[:address].region, - country: opts[:address].country, - postcode: opts[:address].postal_code, - phone: %{ - countryCode: opts[:countryCode], - number: opts[:number] - } - } - } - }) - - commit(:post, "charges/card", body, opts) - 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 - - > Which transactions can be voided? - > Is there a limited time window within which a void can be perfomed? - - ## Example - - > A barebones example using the bindings you've suggested in the `moduledoc`. - """ - @spec void(String.t(), keyword) :: {:ok | :error, Response} - def void(payment_id, opts) do - body = - Poison.encode!(%{ - description: opts[:description], - trackId: opts[:trackId] - }) - - commit(:post, "charges/#{payment_id}/void", body, opts) - end - - @doc """ - Refunds the `amount` to the customer's account with reference to a prior transfer. - - > Refunds are allowed on which kinds of "prior" transactions? - - ## Note - - > The end customer will usually see two bookings/records on his statement. Is - > that true for checkout? - > Is there a limited time window within which a void can be perfomed? - - ## Example - - > A barebones example using the bindings you've suggested in the `moduledoc`. - """ - @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response} - def refund(amount, payment_id, opts) do - {currency, value, _} = Money.to_integer(amount) - - body = - Poison.encode!(%{ - description: opts[:description], - trackId: opts[:trackId], - value: value - }) - - commit(:post, "charges/#{payment_id}/refund", body, opts) - end - - ############################################################################### - # PRIVATE METHODS # - ############################################################################### - - # Makes the request to checkout'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) :: {:ok | :error, Response} - defp commit(:post, endpoint, body, opts) do - url = @test_url <> "#{endpoint}" - - headers = [ - {"Content-Type", "application/json;charset=UTF-8"}, - {"Authorization", opts[:config][:secret_key]} - ] - - HTTPoison.request(:post, url, body, headers) - |> respond - end - - # Parses checkout's response and returns a `Gringotts.Response` struct - # in a `:ok`, `:error` tuple. - @spec respond(term) :: {:ok | :error, Response} - defp respond({:ok, %{status_code: code, body: body}}) when code in 200..299 do - {:ok, parsed} = decode(body) - - id = parsed["id"] - message = parsed["status"] - - { - :ok, - Response.success(id: id, message: message, raw: parsed, status_code: code) - } - end - - defp respond({:ok, %{status_code: status_code, body: body}}) do - {:ok, parsed} = decode(body) - detail = parsed["error_description"] - - { - :error, - Response.error(status_code: status_code, message: detail, raw: body) - } - end - - defp respond({:error, %HTTPoison.Error{} = error}) do - {:error, Response.error(code: error.id, message: "HTTPoison says '#{error.reason}")} - end -end diff --git a/test/integration/gateways/checkout_test.exs b/test/integration/gateways/checkout_test.exs deleted file mode 100644 index f6292b74..00000000 --- a/test/integration/gateways/checkout_test.exs +++ /dev/null @@ -1,140 +0,0 @@ -defmodule Gringotts.Integration.Gateways.CheckoutTest do - # Integration tests for the Checkout - use ExUnit.Case, async: false - use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney - alias Gringotts.Gateways.Checkout - - alias Gringotts.{ - CreditCard, - Address - } - - alias Gringotts.Gateways.Checkout, as: Gateway - - # @moduletag :integration - - @amount Money.new(420, :USD) - - @bad_card1 %CreditCard{ - first_name: "Harry", - last_name: "Potter", - number: "4100000000000001", - year: 2009, - month: 12, - verification_code: "123", - brand: "VISA" - } - - @good_card %CreditCard{ - number: "4543474002249996", - month: 06, - year: 2025, - first_name: "Harry", - last_name: " Potter", - verification_code: "956", - brand: "VISA" - } - - @add %Address{ - street1: "OBH", - street2: "AIT", - city: "PUNE", - region: "MH", - country: "IN", - postal_code: "411015", - phone: "8007810916" - } - - @opts [ - description: "hello", - email: "hi@hello.com", - ip_address: "1.1.1.1", - chargeMode: 1, - config: [ - secret_key: "sk_test_f3695cf1-4f36-485b-bba9-caa5b5acb028" - ], - address: @add - ] - - describe "authorize" do - test "[authorize] with good parameters" do - use_cassette "Checkout/authorize_with_valid_card" do - assert {:ok, response} = Gateway.authorize(@amount, @good_card, @opts) - assert response.success == true - assert response.status_code == 200 - end - end - - test "[authorize] with bad CreditCard" do - use_cassette "Checkout/authorize_with_invalid_card" do - assert {:error, response} = Gateway.authorize(@amount, @bad_card1, @opts) - assert response.success == false - assert response.status_code == 400 - end - end - end - - describe "purchase" do - test "[purchase] with good parameters" do - use_cassette "Checkout/purchase_with_valid_card" do - assert {:ok, response} = Gateway.purchase(@amount, @good_card, @opts) - assert response.success == true - assert response.status_code == 200 - end - end - - test "[purchase] with bad CreditCard" do - use_cassette "Checkout/purchase_with_invalid_card" do - assert {:error, response} = Gateway.purchase(@amount, @bad_card1, @opts) - assert response.success == false - assert response.status_code == 400 - end - end - end - - describe "capture" do - test "[Capture]" do - use_cassette "Checkout/capture" do - assert {:ok, response} = Gateway.authorize(@amount, @good_card, @opts) - assert response.success == true - assert response.status_code == 200 - payment_id = response.id - assert {:ok, response} = Gateway.capture(payment_id, @amount, @opts) - assert response.success == true - assert response.status_code == 200 - end - end - end - - describe "Void" do - test "[Void]" do - use_cassette "Checkout/void" do - assert {:ok, response} = Gateway.authorize(@amount, @good_card, @opts) - assert response.success == true - assert response.status_code == 200 - payment_id = response.id - assert {:ok, response} = Gateway.void(payment_id, @opts) - assert response.success == true - assert response.status_code == 200 - end - end - end - - describe "Refund" do - test "[Refund]" do - use_cassette "Checkout/Refund" do - assert {:ok, response} = Gateway.authorize(@amount, @good_card, @opts) - assert response.success == true - assert response.status_code == 200 - payment_id = response.id - assert {:ok, response} = Gateway.capture(payment_id, @amount, @opts) - assert response.success == true - assert response.status_code == 200 - payment_id = response.id - assert {:ok, response} = Gateway.refund(@amount, payment_id, @opts) - assert response.success == true - assert response.status_code == 200 - end - end - end -end From da252f6f24050dad64db38ded04951830eacb2d5 Mon Sep 17 00:00:00 2001 From: ravirocx Date: Tue, 1 May 2018 02:49:48 +0530 Subject: [PATCH 22/24] implemented all functions with test cases --- lib/gringotts/gateways/checkout.ex | 382 ++++++++++++++++++++ test/integration/gateways/checkout_test.exs | 140 +++++++ 2 files changed, 522 insertions(+) create mode 100644 lib/gringotts/gateways/checkout.ex create mode 100644 test/integration/gateways/checkout_test.exs diff --git a/lib/gringotts/gateways/checkout.ex b/lib/gringotts/gateways/checkout.ex new file mode 100644 index 00000000..89ec5013 --- /dev/null +++ b/lib/gringotts/gateways/checkout.ex @@ -0,0 +1,382 @@ +defmodule Gringotts.Gateways.Checkout do + @moduledoc """ + [checkout][home] gateway implementation. + + ## Instructions! + + ***This is an example `moduledoc`, and suggests some items that should be + documented in here.*** + + The quotation boxes like the one below will guide you in writing excellent + documentation for your gateway. All our gateways are documented in this manner + and we aim to keep our docs as consistent with each other as possible. + **Please read them and do as they suggest**. Feel free to add or skip sections + though. + + If you'd like to make edits to the template docs, they exist at + `templates/gateway.eex`. We encourage you to make corrections and open a PR + and tag it with the label `template`. + + ***Actual docs begin below this line!*** + + -------------------------------------------------------------------------------- + + > List features that have been implemented, and what "actions" they map to as + > per the checkout gateway docs. + > A table suits really well for this. + + ## Optional or extra parameters + + Most `Gringotts` API calls accept an optional `Keyword` list `opts` to supply + optional arguments for transactions with the gateway. + + > List all available (ie, those that will be supported by this module) keys, a + > description of their function/role and whether they have been implemented + > and tested. + > A table suits really well for this. + + ## Registering your checkout account at `Gringotts` + + Explain how to make an account with the gateway and show how to put the + `required_keys` (like authentication info) to the configuration. + + > Here's how the secrets map to the required configuration parameters for checkout: + > + > | Config parameter | checkout secret | + > | ------- | ---- | + > | `:secret_key` | **SecretKey** | + + > Your Application config **must include the `[:secret_key]` field(s)** and would look + > something like this: + > + > config :gringotts, Gringotts.Gateways.Checkout, + > secret_key: "your_secret_secret_key" + + ## Scope of this module + + > It's unlikely that your first iteration will support all features of the + > gateway, so list down those items that are missing. + + ## Supported currencies and countries + + > It's enough if you just add a link to the gateway's docs or FAQ that provide + > info about this. + + ## Following the examples + + 1. First, set up a sample application and configure it to work with checkout. + - 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-monei-account-at-checkout). + + 2. Run an `iex` session with `iex -S mix` and add some variable bindings and + aliases to it (to save some time): + ``` + iex> alias Gringotts.{Response, CreditCard, Gateways.Checkout} + iex> card = %CreditCard{first_name: "Jo", + last_name: "Doe", + number: "4200000000000000", + year: 2099, month: 12, + verification_code: "123", brand: "VISA"} + ``` + + > Add any other frequently used bindings up here. + + We'll be using these in the examples below. + + [gs]: https://github.com/aviabird/gringotts/wiki/ + [home]: https://www.checkout.com + [example]: https://github.com/aviabird/gringotts_example + """ + + # The Base module has the (abstract) public API, and some utility + # implementations. + 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: [:secret_key] + + import Poison, only: [decode: 1] + + alias Gringotts.{Money, CreditCard, Response} + + @test_url "https://sandbox.checkout.com/api2/v2/" + @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. + + > ** You could perhaps:** + > 1. describe what are the important fields in the Response struct + > 2. mention what a merchant can do with these important fields (ex: + > `capture/3`, etc.) + + ## Note + + > If there's anything noteworthy about this operation, it comes here. + + ## Example + + > A barebones example using the bindings you've suggested in the `moduledoc`. + """ + @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} + def authorize(amount, card = %CreditCard{}, opts) do + {currency, value, _} = Money.to_integer(amount) + + body = + Poison.encode!(%{ + email: opts[:email], + currency: currency, + value: value, + autoCapture: "n", + autoCapTime: opts[:autoCapTime], + shippingDetails: %{ + addressLine1: opts[:address].street1, + addressLine2: opts[:address].street2, + city: opts[:address].city, + state: opts[:address].region, + country: opts[:address].country, + postcode: opts[:address].postal_code, + phone: %{ + countryCode: opts[:countryCode], + number: opts[:number] + } + }, + chargeMode: opts[:chargeMode], + customerIp: opts[:customerIp], + customerName: opts[:customerName], + description: opts[:description], + descriptor: opts[:descriptor], + trackId: opts[:trackId], + card: %{ + number: card.number, + name: CreditCard.full_name(card), + cvv: card.verification_code, + expiryMonth: card.month, + expiryYear: card.year, + billingDetails: %{ + addressLine1: opts[:address].street1, + addressLine2: opts[:address].street2, + city: opts[:address].city, + state: opts[:address].region, + country: opts[:address].country, + postcode: opts[:address].postal_code, + phone: %{ + countryCode: opts[:countryCode], + number: opts[:number] + } + } + } + }) + + commit(:post, "charges/card", body, opts) + end + + @doc """ + Captures a pre-authorized `amount`. + + `amount` is transferred to the merchant account by checkout used in the + pre-authorization referenced by `payment_id`. + + ## Note + + > If there's anything noteworthy about this operation, it comes here. + > For example, does the gateway support partial, multiple captures? + + ## Example + + > A barebones example using the bindings you've suggested in the `moduledoc`. + """ + @spec capture(String.t(), Money.t(), keyword) :: {:ok | :error, Response} + def capture(payment_id, amount, opts) do + {currency, value, _} = Money.to_integer(amount) + + body = + Poison.encode!(%{ + description: opts[:description], + trackId: opts[:trackId], + value: value + }) + + commit(:post, "charges/#{payment_id}/capture", body, opts) + end + + @doc """ + Transfers `amount` from the customer to the merchant. + + checkout attempts to process a purchase on behalf of the customer, by + debiting `amount` from the customer's account by charging the customer's + `card`. + + ## Note + + > If there's anything noteworthy about this operation, it comes here. + + ## Example + + > A barebones example using the bindings you've suggested in the `moduledoc`. + """ + @spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} + def purchase(amount, card = %CreditCard{}, opts) do + {currency, value, _} = Money.to_integer(amount) + + body = + Poison.encode!(%{ + email: opts[:email], + currency: currency, + value: value, + autoCapTime: opts[:autoCapTime], + shippingDetails: %{ + addressLine1: opts[:address].street1, + addressLine2: opts[:address].street2, + city: opts[:address].city, + state: opts[:address].region, + country: opts[:address].country, + postcode: opts[:address].postal_code, + phone: %{ + countryCode: opts[:countryCode], + number: opts[:number] + } + }, + chargeMode: opts[:chargeMode], + customerIp: opts[:customerIp], + customerName: opts[:customerName], + description: opts[:description], + descriptor: opts[:descriptor], + trackId: opts[:trackId], + card: %{ + number: card.number, + name: CreditCard.full_name(card), + cvv: card.verification_code, + expiryMonth: card.month, + expiryYear: card.year, + billingDetails: %{ + addressLine1: opts[:address].street1, + addressLine2: opts[:address].street2, + city: opts[:address].city, + state: opts[:address].region, + country: opts[:address].country, + postcode: opts[:address].postal_code, + phone: %{ + countryCode: opts[:countryCode], + number: opts[:number] + } + } + } + }) + + commit(:post, "charges/card", body, opts) + 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 + + > Which transactions can be voided? + > Is there a limited time window within which a void can be perfomed? + + ## Example + + > A barebones example using the bindings you've suggested in the `moduledoc`. + """ + @spec void(String.t(), keyword) :: {:ok | :error, Response} + def void(payment_id, opts) do + body = + Poison.encode!(%{ + description: opts[:description], + trackId: opts[:trackId] + }) + + commit(:post, "charges/#{payment_id}/void", body, opts) + end + + @doc """ + Refunds the `amount` to the customer's account with reference to a prior transfer. + + > Refunds are allowed on which kinds of "prior" transactions? + + ## Note + + > The end customer will usually see two bookings/records on his statement. Is + > that true for checkout? + > Is there a limited time window within which a void can be perfomed? + + ## Example + + > A barebones example using the bindings you've suggested in the `moduledoc`. + """ + @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response} + def refund(amount, payment_id, opts) do + {currency, value, _} = Money.to_integer(amount) + + body = + Poison.encode!(%{ + description: opts[:description], + trackId: opts[:trackId], + value: value + }) + + commit(:post, "charges/#{payment_id}/refund", body, opts) + end + + ############################################################################### + # PRIVATE METHODS # + ############################################################################### + + # Makes the request to checkout'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) :: {:ok | :error, Response} + defp commit(:post, endpoint, body, opts) do + url = @test_url <> "#{endpoint}" + + headers = [ + {"Content-Type", "application/json;charset=UTF-8"}, + {"Authorization", opts[:config][:secret_key]} + ] + + HTTPoison.request(:post, url, body, headers) + |> respond + end + + # Parses checkout's response and returns a `Gringotts.Response` struct + # in a `:ok`, `:error` tuple. + @spec respond(term) :: {:ok | :error, Response} + defp respond({:ok, %{status_code: code, body: body}}) when code in 200..299 do + {:ok, parsed} = decode(body) + + id = parsed["id"] + message = parsed["status"] + + { + :ok, + Response.success(id: id, message: message, raw: parsed, status_code: code) + } + end + + defp respond({:ok, %{status_code: status_code, body: body}}) do + {:ok, parsed} = decode(body) + detail = parsed["error_description"] + + { + :error, + Response.error(status_code: status_code, message: detail, raw: body) + } + end + + defp respond({:error, %HTTPoison.Error{} = error}) do + {:error, Response.error(code: error.id, message: "HTTPoison says '#{error.reason}")} + end +end diff --git a/test/integration/gateways/checkout_test.exs b/test/integration/gateways/checkout_test.exs new file mode 100644 index 00000000..f6292b74 --- /dev/null +++ b/test/integration/gateways/checkout_test.exs @@ -0,0 +1,140 @@ +defmodule Gringotts.Integration.Gateways.CheckoutTest do + # Integration tests for the Checkout + use ExUnit.Case, async: false + use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney + alias Gringotts.Gateways.Checkout + + alias Gringotts.{ + CreditCard, + Address + } + + alias Gringotts.Gateways.Checkout, as: Gateway + + # @moduletag :integration + + @amount Money.new(420, :USD) + + @bad_card1 %CreditCard{ + first_name: "Harry", + last_name: "Potter", + number: "4100000000000001", + year: 2009, + month: 12, + verification_code: "123", + brand: "VISA" + } + + @good_card %CreditCard{ + number: "4543474002249996", + month: 06, + year: 2025, + first_name: "Harry", + last_name: " Potter", + verification_code: "956", + brand: "VISA" + } + + @add %Address{ + street1: "OBH", + street2: "AIT", + city: "PUNE", + region: "MH", + country: "IN", + postal_code: "411015", + phone: "8007810916" + } + + @opts [ + description: "hello", + email: "hi@hello.com", + ip_address: "1.1.1.1", + chargeMode: 1, + config: [ + secret_key: "sk_test_f3695cf1-4f36-485b-bba9-caa5b5acb028" + ], + address: @add + ] + + describe "authorize" do + test "[authorize] with good parameters" do + use_cassette "Checkout/authorize_with_valid_card" do + assert {:ok, response} = Gateway.authorize(@amount, @good_card, @opts) + assert response.success == true + assert response.status_code == 200 + end + end + + test "[authorize] with bad CreditCard" do + use_cassette "Checkout/authorize_with_invalid_card" do + assert {:error, response} = Gateway.authorize(@amount, @bad_card1, @opts) + assert response.success == false + assert response.status_code == 400 + end + end + end + + describe "purchase" do + test "[purchase] with good parameters" do + use_cassette "Checkout/purchase_with_valid_card" do + assert {:ok, response} = Gateway.purchase(@amount, @good_card, @opts) + assert response.success == true + assert response.status_code == 200 + end + end + + test "[purchase] with bad CreditCard" do + use_cassette "Checkout/purchase_with_invalid_card" do + assert {:error, response} = Gateway.purchase(@amount, @bad_card1, @opts) + assert response.success == false + assert response.status_code == 400 + end + end + end + + describe "capture" do + test "[Capture]" do + use_cassette "Checkout/capture" do + assert {:ok, response} = Gateway.authorize(@amount, @good_card, @opts) + assert response.success == true + assert response.status_code == 200 + payment_id = response.id + assert {:ok, response} = Gateway.capture(payment_id, @amount, @opts) + assert response.success == true + assert response.status_code == 200 + end + end + end + + describe "Void" do + test "[Void]" do + use_cassette "Checkout/void" do + assert {:ok, response} = Gateway.authorize(@amount, @good_card, @opts) + assert response.success == true + assert response.status_code == 200 + payment_id = response.id + assert {:ok, response} = Gateway.void(payment_id, @opts) + assert response.success == true + assert response.status_code == 200 + end + end + end + + describe "Refund" do + test "[Refund]" do + use_cassette "Checkout/Refund" do + assert {:ok, response} = Gateway.authorize(@amount, @good_card, @opts) + assert response.success == true + assert response.status_code == 200 + payment_id = response.id + assert {:ok, response} = Gateway.capture(payment_id, @amount, @opts) + assert response.success == true + assert response.status_code == 200 + payment_id = response.id + assert {:ok, response} = Gateway.refund(@amount, payment_id, @opts) + assert response.success == true + assert response.status_code == 200 + end + end + end +end From fca1c677ed4aac823551bcba5622c5000bb1a39e Mon Sep 17 00:00:00 2001 From: ravirocx Date: Thu, 3 May 2018 02:23:54 +0530 Subject: [PATCH 23/24] Added docs --- lib/gringotts/gateways/checkout.ex | 145 +++++++++++------------------ 1 file changed, 55 insertions(+), 90 deletions(-) diff --git a/lib/gringotts/gateways/checkout.ex b/lib/gringotts/gateways/checkout.ex index 89ec5013..73c45935 100644 --- a/lib/gringotts/gateways/checkout.ex +++ b/lib/gringotts/gateways/checkout.ex @@ -2,49 +2,39 @@ defmodule Gringotts.Gateways.Checkout do @moduledoc """ [checkout][home] gateway implementation. - ## Instructions! + A module for working with the checkout payment gateway. - ***This is an example `moduledoc`, and suggests some items that should be - documented in here.*** + Refer the official Checkout [API docs][docs]. - The quotation boxes like the one below will guide you in writing excellent - documentation for your gateway. All our gateways are documented in this manner - and we aim to keep our docs as consistent with each other as possible. - **Please read them and do as they suggest**. Feel free to add or skip sections - though. + The following set of functions for Checkout have been implemented: - If you'd like to make edits to the template docs, they exist at - `templates/gateway.eex`. We encourage you to make corrections and open a PR - and tag it with the label `template`. + | Action | Method | + | ------ | ------ | + | Authorize a Credit Card | `authorize/3` | + | Capture a previously authorized amount | `capture/3` | + | Charge a Credit Card | `purchase/3` | + | Refund a transaction | `refund/3` | + | Void a transaction | `void/2` | - ***Actual docs begin below this line!*** - - -------------------------------------------------------------------------------- - - > List features that have been implemented, and what "actions" they map to as - > per the checkout gateway docs. - > A table suits really well for this. ## Optional or extra parameters Most `Gringotts` API calls accept an optional `Keyword` list `opts` to supply optional arguments for transactions with the gateway. + To know more about these keywords visit [Request and Response][req-resp] tabs for each + API method. - > List all available (ie, those that will be supported by this module) keys, a - > description of their function/role and whether they have been implemented - > and tested. - > A table suits really well for this. + [req-resp]: https://beta.docs.checkout.com/docs/payments-quickstart - ## Registering your checkout account at `Gringotts` - Explain how to make an account with the gateway and show how to put the - `required_keys` (like authentication info) to the configuration. + ## Registering your checkout account at `Gringotts` + > Here's how the secrets map to the required configuration parameters for checkout: > > | Config parameter | checkout secret | - > | ------- | ---- | - > | `:secret_key` | **SecretKey** | + > | -------------- | ----------- | + > | `:secret_key` | **SecretKey** | > Your Application config **must include the `[:secret_key]` field(s)** and would look > something like this: @@ -52,15 +42,11 @@ defmodule Gringotts.Gateways.Checkout do > config :gringotts, Gringotts.Gateways.Checkout, > secret_key: "your_secret_secret_key" - ## Scope of this module - - > It's unlikely that your first iteration will support all features of the - > gateway, so list down those items that are missing. - + ## Supported currencies and countries - > It's enough if you just add a link to the gateway's docs or FAQ that provide - > info about this. + > * Europe + > * North America ## Following the examples @@ -73,6 +59,7 @@ defmodule Gringotts.Gateways.Checkout do 2. Run an `iex` session with `iex -S mix` and add some variable bindings and aliases to it (to save some time): + We'll be using these in the examples below. ``` iex> alias Gringotts.{Response, CreditCard, Gateways.Checkout} iex> card = %CreditCard{first_name: "Jo", @@ -82,10 +69,10 @@ defmodule Gringotts.Gateways.Checkout do verification_code: "123", brand: "VISA"} ``` - > Add any other frequently used bindings up here. - + We'll be using these in the examples below. + [docs]: https://beta.docs.checkout.com/docs [gs]: https://github.com/aviabird/gringotts/wiki/ [home]: https://www.checkout.com [example]: https://github.com/aviabird/gringotts_example @@ -107,22 +94,17 @@ defmodule Gringotts.Gateways.Checkout do @test_url "https://sandbox.checkout.com/api2/v2/" @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. - - > ** You could perhaps:** - > 1. describe what are the important fields in the Response struct - > 2. mention what a merchant can do with these important fields (ex: - > `capture/3`, etc.) - - ## Note - - > If there's anything noteworthy about this operation, it comes here. - + Checkout returns an ID string which can be used to: + * `capture/3` _an_ amount. + * `void/2` an amount ## Example - - > A barebones example using the bindings you've suggested in the `moduledoc`. + ``` + iex> amount = Money.new(42, :USD) + iex> {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.Checkout, amount, card, opts) + iex> auth_result.id # This is the charge ID + ``` """ @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} def authorize(amount, card = %CreditCard{}, opts) do @@ -179,18 +161,14 @@ defmodule Gringotts.Gateways.Checkout do @doc """ Captures a pre-authorized `amount`. - - `amount` is transferred to the merchant account by checkout used in the - pre-authorization referenced by `payment_id`. - + `amount` is transferred to the merchant account by Checkout used in the + pre-authorization referenced by `charge_id`. ## Note - - > If there's anything noteworthy about this operation, it comes here. - > For example, does the gateway support partial, multiple captures? - + > Checkout **do** support partial captures, but only once per authorized payment. ## Example - - > A barebones example using the bindings you've suggested in the `moduledoc`. + ``` + iex> {:ok, capture_result} = Gringotts.capture(Gringotts.Gateways.Checkout, amount, auth_result.id, opts) + ``` """ @spec capture(String.t(), Money.t(), keyword) :: {:ok | :error, Response} def capture(payment_id, amount, opts) do @@ -208,18 +186,14 @@ defmodule Gringotts.Gateways.Checkout do @doc """ Transfers `amount` from the customer to the merchant. - - checkout attempts to process a purchase on behalf of the customer, by + Checkout attempts to process a purchase on behalf of the customer, by debiting `amount` from the customer's account by charging the customer's `card`. - - ## Note - - > If there's anything noteworthy about this operation, it comes here. - ## Example - - > A barebones example using the bindings you've suggested in the `moduledoc`. + ``` + iex> amount = Money.new(42, :USD) + iex> {:ok, purchase_result} = Gringotts.purchase(Gringotts.Gateways.Checkout, amount, card, opts) + iex> purchase_result.id # This is the charge ID """ @spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} def purchase(amount, card = %CreditCard{}, opts) do @@ -275,20 +249,16 @@ defmodule Gringotts.Gateways.Checkout do @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. - + `charge_id`. + ## Note - - > Which transactions can be voided? - > Is there a limited time window within which a void can be perfomed? - + > As a consequence, the customer will never see any booking on his statement. + > Checkout must be in authorized state and **not** in captured state. ## Example - - > A barebones example using the bindings you've suggested in the `moduledoc`. + ``` + iex> {:ok, void_result} = Gringotts.capture(Gringotts.Gateways.Checkout, purchase_result.id, opts) + ``` """ @spec void(String.t(), keyword) :: {:ok | :error, Response} def void(payment_id, opts) do @@ -301,20 +271,15 @@ defmodule Gringotts.Gateways.Checkout do commit(:post, "charges/#{payment_id}/void", body, opts) end - @doc """ + @doc """ Refunds the `amount` to the customer's account with reference to a prior transfer. - - > Refunds are allowed on which kinds of "prior" transactions? - + > Refunds are allowed on Captured / purchased transraction. ## Note - - > The end customer will usually see two bookings/records on his statement. Is - > that true for checkout? - > Is there a limited time window within which a void can be perfomed? - + * Checkout does support partial refunds, but only once per captured payment. ## Example - - > A barebones example using the bindings you've suggested in the `moduledoc`. + ``` + iex> {:ok, refund_result} = Gringotts.refund(Gringotts.Gateways.Checkout, purchase_result.id, amount) + ``` """ @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response} def refund(amount, payment_id, opts) do From 6ff6f73bd7bd480460043fc5a99964b876df6087 Mon Sep 17 00:00:00 2001 From: ravirocx Date: Thu, 3 May 2018 02:30:10 +0530 Subject: [PATCH 24/24] improved code readability --- lib/gringotts/gateways/checkout.ex | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/gringotts/gateways/checkout.ex b/lib/gringotts/gateways/checkout.ex index 73c45935..facb2876 100644 --- a/lib/gringotts/gateways/checkout.ex +++ b/lib/gringotts/gateways/checkout.ex @@ -16,7 +16,6 @@ defmodule Gringotts.Gateways.Checkout do | Refund a transaction | `refund/3` | | Void a transaction | `void/2` | - ## Optional or extra parameters Most `Gringotts` API calls accept an optional `Keyword` list `opts` to supply @@ -26,10 +25,8 @@ defmodule Gringotts.Gateways.Checkout do [req-resp]: https://beta.docs.checkout.com/docs/payments-quickstart + ## Registering your checkout account at `Gringotts` - ## Registering your checkout account at `Gringotts` - - > Here's how the secrets map to the required configuration parameters for checkout: > > | Config parameter | checkout secret | @@ -42,7 +39,6 @@ defmodule Gringotts.Gateways.Checkout do > config :gringotts, Gringotts.Gateways.Checkout, > secret_key: "your_secret_secret_key" - ## Supported currencies and countries > * Europe @@ -69,7 +65,6 @@ defmodule Gringotts.Gateways.Checkout do verification_code: "123", brand: "VISA"} ``` - We'll be using these in the examples below. [docs]: https://beta.docs.checkout.com/docs @@ -251,7 +246,7 @@ defmodule Gringotts.Gateways.Checkout do Voids the referenced payment. This method attempts a reversal of a previous transaction referenced by `charge_id`. - + ## Note > As a consequence, the customer will never see any booking on his statement. > Checkout must be in authorized state and **not** in captured state. @@ -271,7 +266,7 @@ defmodule Gringotts.Gateways.Checkout do commit(:post, "charges/#{payment_id}/void", body, opts) end - @doc """ + @doc """ Refunds the `amount` to the customer's account with reference to a prior transfer. > Refunds are allowed on Captured / purchased transraction. ## Note