Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[stripe] added parsing response into Response and tests #203

Open
wants to merge 8 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 179 additions & 47 deletions lib/gringotts/gateways/stripe.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ defmodule Gringotts.Gateways.Stripe do
| `statement_descriptor` | **Implemented** |
| `charge` | **Implemented** |
| `reason` | **Implemented** |
| `confirm` | **Implemented** |
| `confirmation_method` | **Implemented** |
| `return_url` | **Implemented** |
| `account_balance` | Not implemented |
| `business_vat_id` | Not implemented |
| `coupon` | Not implemented |
Expand Down Expand Up @@ -80,7 +83,7 @@ defmodule Gringotts.Gateways.Stripe do
use Gringotts.Gateways.Base
use Gringotts.Adapter, required_config: [:secret_key]

alias Gringotts.{Address, CreditCard, Money}
alias Gringotts.{Address, CreditCard, Money, Response}

@doc """
Performs a (pre) Authorize operation.
Expand Down Expand Up @@ -125,10 +128,11 @@ defmodule Gringotts.Gateways.Stripe do

iex> Gringotts.authorize(Gringotts.Gateways.Stripe, amount, card, opts)
"""
@spec authorize(Money.t(), CreditCard.t() | String.t(), keyword) :: map
@spec authorize(Money.t(), CreditCard.t() | String.t(), keyword) :: {:ok | :error, Response.t()}
def authorize(amount, payment, opts) do
params = create_params_for_auth_or_purchase(amount, payment, opts, false)
commit(:post, "charges", params, opts)
params = create_params_for(amount, payment, opts, :authorize)
response = commit(:post, "charges", params, opts)
respond(response, :purchase)
end

@doc """
Expand Down Expand Up @@ -163,10 +167,27 @@ defmodule Gringotts.Gateways.Stripe do

iex> Gringotts.purchase(Gringotts.Gateways.Stripe, amount, card, opts)
"""
@spec purchase(Money.t(), CreditCard.t() | String.t(), keyword) :: map
@spec purchase(Money.t(), CreditCard.t() | String.t(), keyword) :: {:ok | :error, Response.t()}
def purchase(amount, payment, opts) do
params = create_params_for_auth_or_purchase(amount, payment, opts)
commit(:post, "charges", params, opts)
params = create_params_for(amount, payment, opts, :purchase)

cond do
is_struct(payment) && payment.__struct__ == CreditCard ->
response = commit(:post, "payment_methods", params, opts)
respond(response, :payment_method)

String.starts_with?(payment, "pm") ->
response = commit(:post, "payment_intents", params, opts)
respond(response, :payment_intent)

String.starts_with?(payment, "pi") ->
response = commit(:post, "payment_intents/#{payment}/confirm", [], opts)
maybe_get_latest_charge(response, opts)

String.starts_with?(payment, "tok") ->
response = commit(:post, "charges", params, opts)
respond(response, :purchase)
end
end

@doc """
Expand All @@ -190,10 +211,11 @@ defmodule Gringotts.Gateways.Stripe do

iex> Gringotts.capture(Gringotts.Gateways.Stripe, id, amount, opts)
"""
@spec capture(String.t(), Money.t(), keyword) :: map
@spec capture(String.t(), Money.t(), keyword) :: {:ok | :error, Response.t()}
def capture(id, amount, opts) do
params = optional_params(opts) ++ amount_params(amount)
commit(:post, "charges/#{id}/capture", params, opts)
response = commit(:post, "charges/#{id}/capture", params, opts)
respond(response, :purchase)
end

@doc """
Expand Down Expand Up @@ -223,10 +245,9 @@ defmodule Gringotts.Gateways.Stripe do

iex> Gringotts.void(Gringotts.Gateways.Stripe, id, opts)
"""
@spec void(String.t(), keyword) :: map
def void(id, opts) do
params = optional_params(opts)
commit(:post, "charges/#{id}/refund", params, opts)
@spec void(Money.t(), String.t(), keyword) :: {:ok | :error, Response.t()}
def void(amount, charge_id, opts \\ []) do
refund(amount, charge_id, opts)
end

@doc """
Expand All @@ -245,10 +266,14 @@ defmodule Gringotts.Gateways.Stripe do

iex> Gringotts.refund(Gringotts.Gateways.Stripe, amount, id, opts)
"""
@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)
@spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response.t()}
def refund(amount, charge_id, opts \\ []) do
params =
[charge: charge_id] ++
Keyword.take(amount_params(amount), [:amount])

response = commit(:post, "refunds", params, opts)
respond(response, :refund)
end

@doc """
Expand Down Expand Up @@ -283,7 +308,7 @@ defmodule Gringotts.Gateways.Stripe do

iex> Gringotts.store(Gringotts.Gateways.Stripe, card, opts)
"""
@spec store(CreditCard.t() | String.t(), keyword) :: map
@spec store(CreditCard.t() | String.t(), keyword) :: {:ok | :error, Response.t()}
def store(payment, opts) do
params = optional_params(opts) ++ source_params(payment, opts)
commit(:post, "customers", params, opts)
Expand All @@ -303,18 +328,42 @@ defmodule Gringotts.Gateways.Stripe do

iex> Gringotts.unstore(Gringotts.Gateways.Stripe, id, opts)
"""
@spec unstore(String.t(), keyword) :: map
@spec unstore(String.t(), keyword) :: {:ok | :error, Response.t()}
def unstore(id, opts), do: commit(:delete, "customers/#{id}", [], opts)

# Private methods

defp create_params_for_auth_or_purchase(amount, payment, opts, capture \\ true) do
[capture: capture] ++
defp create_params_for(
amount,
payment,
opts,
action
) do
variable_params =
cond do
action == :authorize ->
[capture: false]

action == :purchase && String.starts_with?(payment, "tok") ->
[capture: true]

String.starts_with?(payment, "pm") ->
[
confirm: true,
confirmation_method: "manual"
]
|> append_if(opts[:return_url], return_url: opts[:return_url])

true ->
[]
end

variable_params ++
optional_params(opts) ++ amount_params(amount) ++ source_params(payment, opts)
end

defp create_card_token(params, opts) do
commit(:post, "tokens", params, opts)
commit(:post, "payment_methods", params, opts)
end

defp amount_params(amount) do
Expand All @@ -323,9 +372,11 @@ defmodule Gringotts.Gateways.Stripe do
end

defp source_params(token_or_customer, _) when is_binary(token_or_customer) do
[head, _] = String.split(token_or_customer, "_")
[head | _] = String.split(token_or_customer, "_")

case head do
"pi" -> []
"pm" -> [payment_method: token_or_customer]
"tok" -> [source: token_or_customer]
"cus" -> [customer: token_or_customer]
end
Expand All @@ -334,40 +385,35 @@ defmodule Gringotts.Gateways.Stripe do
defp source_params(%CreditCard{} = card, opts) do
params = card_params(card) ++ address_params(opts[:address])

response = create_card_token(params, opts)

if Map.has_key?(response, "error") do
[]
with {:ok, response} <- create_card_token(params, opts) do
source_params(response.id, opts)
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),
type: "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_line2]": address.street2,
"card[address_city]": address.city,
"card[address_state]": address.region,
"card[address_zip]": address.postal_code,
"card[address_country]": address.country
]
[]
# [
# "card[address_line1]": address.street1,
# "card[address_line2]": address.street2,
# "card[address_city]": address.city,
# "card[address_state]": address.region,
# "card[address_zip]": address.postal_code,
# "card[address_country]": address.country
# ]
end

defp address_params(_), do: []
Expand All @@ -380,8 +426,7 @@ defmodule Gringotts.Gateways.Stripe do
{"Authorization", auth_token}
]

response = HTTPoison.request(method, "#{@base_url}/#{path}", {:form, params}, headers)
format_response(response)
HTTPoison.request(method, "#{@base_url}/#{path}", {:form, params}, headers)
end

defp optional_params(opts) do
Expand All @@ -390,10 +435,97 @@ defmodule Gringotts.Gateways.Stripe do
|> Keyword.delete(:address)
end

defp format_response(response) do
case response do
{:ok, %HTTPoison.Response{body: body}} -> body |> Jason.decode!()
_ -> %{"error" => "something went wrong, please try again later"}
# Parses STRIPE's response and returns a `Gringotts.Response` struct in a
# `:ok`, `:error` tuple.
@spec respond(term, Atom.t()) :: {:ok | :error, Response.t()}
defp respond(stripe_response, gringotts_action)

defp respond({:ok, %{status_code: 200, body: body}}, gringotts_action) do
common = [raw: body, status_code: 200, gateway_code: 200]

with {:ok, decoded_json} <- Jason.decode(body),
{:ok, results} <- parse_response(decoded_json, gringotts_action) do
{:ok, Response.success(common ++ results)}
else
{:not_ok, errors} ->
{:ok, Response.error(common ++ errors)}

{:error, _} ->
{:error, Response.error([reason: "undefined response from stripe"] ++ common)}
end
end

defp respond({:ok, %{status_code: status_code, body: body}}, _) do
{:error, Response.error(status_code: status_code, raw: body)}
end

defp respond({:error, %HTTPoison.Error{} = error}, _) do
{
:error,
Response.error(
reason: "network related failure",
message: "HTTPoison says '#{error.reason}' [ID: #{error.id || "nil"}]"
)
}
end

defp parse_response(data, gringotts_action) when gringotts_action == :purchase do
address = struct(%Address{}, data["billing_details"]["address"])

results = [
id: data["id"],
token: data["balance_transaction"],
message: data["description"],
fraud_review: data["outcome"]["risk_level"],
cvc_result: data["payment_method_details"]["card"]["checks"]["cvc_check"],
avs_result: %{address: address}
]

{:ok, results}
end

defp parse_response(data, gringotts_action) when gringotts_action == :payment_intent do
results = [
id: data["id"],
token: data["client_secret"],
message: data["latest_charge"],
reason: data["status"],
next_action: data["next_action"],
fraud_review: nil,
cvc_result: nil,
avs_result: nil
]

{:ok, results}
end

defp parse_response(data, gringotts_action) when gringotts_action == :refund do
results = [
id: data["id"],
token: data["balance_transaction"],
message: data["reason"]
]

{:ok, results}
end

defp maybe_get_latest_charge(response, opts) do
with {:ok, _} <- response,
{:ok, parsed_response} <- respond(response, :payment_intent) do
charge = parsed_response.message

if is_nil(charge) do
{:ok, parsed_response}
else
response = commit(:get, "charges/#{charge}", [], opts)
respond(response, :purchase)
end
else
_ -> respond(response, :payment_intent)
end
end

defp append_if(list, condition, item) do
if condition, do: list ++ item, else: list
end
end
14 changes: 11 additions & 3 deletions lib/gringotts/response.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ defmodule Gringotts.Response do
:avs_result,
:cvc_result,
:raw,
:fraud_review
:fraud_review,
:status,
:next_action
]

@typedoc """
Expand All @@ -43,7 +45,11 @@ defmodule Gringotts.Response do
case of error. `nil` otherwise. |
| `raw` | `String.t` | Raw response from the gateway. |
| `fraud_review` | `term` | Gateway's risk assessment of the\
transaction. |
transaction. |
| `status` | `String.t` | Status of the performed action. |
| `next_action` | `map` | Necesssary data to perform other\
actions in order to complete\
payment` |

## Notes

Expand Down Expand Up @@ -74,7 +80,9 @@ defmodule Gringotts.Response do
avs_result: %{street: String.t(), zip_code: String.t()},
cvc_result: String.t(),
raw: String.t(),
fraud_review: term
fraud_review: term,
status: String.t(),
next_action: Map.t()
}

def success(opts \\ []) do
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ defmodule Gringotts.Mixfile do
# Type `mix help deps` for more examples and options
defp deps do
[
{:httpoison, "~> 1.8"},
{:httpoison, "~> 2.2"},
{:jason, "~> 1.4"},
{:xml_builder, "~> 2.2"},
{:elixir_xml_to_map, "~> 3.0"},
Expand Down
Loading