Skip to content

Commit

Permalink
Adapts Stripe with the money protocol
Browse files Browse the repository at this point in the history
* Fixes #5
* Moved the stripe_test to integration.
* Fixed credo issue in money integration test
  • Loading branch information
oyeb committed Jan 29, 2018
1 parent 5b15d30 commit 9f7fa43
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 134 deletions.
35 changes: 14 additions & 21 deletions lib/gringotts/gateways/base.ex
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)}
Expand Down
107 changes: 53 additions & 54 deletions lib/gringotts/gateways/stripe.ex
Original file line number Diff line number Diff line change
@@ -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` |
Expand All @@ -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** |
Expand All @@ -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,
adapter: Gringotts.Gateways.Stripe,
secret_key: "your_secret_key",
Expand All @@ -59,11 +58,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 """
Expand All @@ -72,17 +72,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",
Expand All @@ -105,18 +105,18 @@ 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)
end

@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.
Expand All @@ -143,7 +143,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)
Expand All @@ -169,15 +169,15 @@ 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)
end

@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
Expand All @@ -191,7 +191,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
Expand Down Expand Up @@ -224,15 +224,15 @@ 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)
end

@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.
Expand Down Expand Up @@ -286,92 +286,91 @@ 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]
end
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
4 changes: 4 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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", [], [], "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"},
Expand All @@ -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", [], [], "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"},
Expand All @@ -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.24", "d198ae9783ac807721cca0c5535384ebdf99da4976be8cefb9665a9262a1e9e3", [], [{:combine, "~> 0.7", [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", [], [{: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"}}
Loading

0 comments on commit 9f7fa43

Please sign in to comment.