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