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

Reorganize Payment Gateway Structure and Fix Payment Method Creation #24

Merged
merged 6 commits into from Apr 19, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Expand Up @@ -53,7 +53,9 @@ config :ryal_core,
user_module: App.User,
user_table: :users,
default_payment_gateway: :bogus,
fallback_payment_gateways: [:stripe, :braintree]
payment_gateways: %{
stripe: "sk_test_123"
}
```

Now you'll want to copy over the migrations.
Expand Down
21 changes: 14 additions & 7 deletions apps/ryal_core/lib/ryal.ex
Expand Up @@ -3,15 +3,22 @@ defmodule Ryal do
The core Ryal namespace. This guy is primarily used for configuration.
"""

@payment_gateway Application.get_env(:ryal_core, :payment_gateway)
@repo Application.get_env(:ryal_core, :repo)
@user_module Application.get_env(:ryal_core, :user_module)
@user_table Application.get_env(:ryal_core, :user_table)

use Application

def payment_gateway, do: @payment_gateway
def payment_gateway_keys, do: Map.get(@payment_gateway, :keys) || %{}
import Application, only: [get_env: 2]

@default_payment_gateway get_env(:ryal_core, :default_payment_gateway)
@payment_gateways get_env(:ryal_core, :payment_gateways)

@repo get_env(:ryal_core, :repo)
@user_module get_env(:ryal_core, :user_module)
@user_table get_env(:ryal_core, :user_table)

def payment_gateways, do: @payment_gateways
def default_payment_gateway, do: @default_payment_gateway
def fallback_gateways do
Map.keys(payment_gateways() || %{}) -- [default_payment_gateway()]
end

def repo, do: @repo
def user_module, do: @user_module
Expand Down
25 changes: 17 additions & 8 deletions apps/ryal_core/lib/ryal/web/commands/payment_gateway_command.ex
Expand Up @@ -5,17 +5,16 @@ defmodule Ryal.PaymentGatewayCommand do
"""

alias Ryal.PaymentGateway
alias Ryal.PaymentGateway.Customer

@default_gateway Map.get(Ryal.payment_gateway, :default)
@fallback_gateways Map.get(Ryal.payment_gateway, :fallbacks)

@doc "Shorthand for creating all the payment gateways relevant to a user."
@spec create(Ecto.Schema.t) ::
{:ok, Ecto.Schema.t} | {:error, Ecto.Changeset.t}
def create(user) do
Enum.each @fallback_gateways || [], fn(gateway_type) ->
Enum.each Ryal.fallback_gateways() || [], fn(gateway_type) ->
spawn_monitor fn -> create(gateway_type, user) end
end

create @default_gateway, user
create Ryal.default_payment_gateway(), user
end

@doc """
Expand All @@ -27,7 +26,7 @@ defmodule Ryal.PaymentGatewayCommand do
def create(type, user) do
struct = %PaymentGateway{type: Atom.to_string(type), user_id: user.id}

with {:ok, external_id} <- Customer.create(type, user),
with {:ok, external_id} <- payment_gateway(type).create(:customer, user),
changeset <-
PaymentGateway.changeset(%{struct | external_id: external_id}),
do: Ryal.repo.insert(changeset)
Expand All @@ -52,7 +51,17 @@ defmodule Ryal.PaymentGatewayCommand do

Enum.map user.payment_gateways, fn(payment_gateway) ->
type = String.to_atom payment_gateway.type
spawn_monitor Customer, action, [type, payment_gateway]
spawn_monitor payment_gateway(type), action, [:customer, payment_gateway]
end
end

defp payment_gateway(type) do
module_type = type
|> Atom.to_string
|> Macro.camelize

{module, []} = Code.eval_string("Ryal.PaymentGateway.#{module_type}")

module
end
end
25 changes: 25 additions & 0 deletions apps/ryal_core/lib/ryal/web/models/payment_gateways/bogus.ex
@@ -0,0 +1,25 @@
defmodule Ryal.PaymentGateway.Bogus do
@moduledoc """
A very simple payment gateway module that provides the basic functions to fake
create, update, and delete methods with a payment gateway.
"""

@doc "Simple bogus create function for an external_id."
@spec create(atom, Ecto.Schema.t) :: {:ok, String.t}
def create(_atom, _schema), do: {:ok, random_id()}

@doc "Simple bogus update function."
@spec update(atom, Ecto.Schema.t) :: {:ok, %{}}
def update(_atom, _schema), do: {:ok, %{}}

@doc "Simple bogus delete function."
@spec delete(atom, Ecto.Schema.t) :: {:ok, %{}}
def delete(_atom, _schema), do: {:ok, %{}}

defp random_id do
:rand.uniform * 10_000_000_000
|> round

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pipe chain should start with a raw value.

|> to_string
|> String.ljust(10, ?0)
end
end
67 changes: 0 additions & 67 deletions apps/ryal_core/lib/ryal/web/models/payment_gateways/customer.ex

This file was deleted.

76 changes: 76 additions & 0 deletions apps/ryal_core/lib/ryal/web/models/payment_gateways/stripe.ex
@@ -0,0 +1,76 @@
defmodule Ryal.PaymentGateway.Stripe do
@moduledoc "Relevant functions for working with the Stipe API."

alias Ryal.PaymentGatewayQuery

@stripe_api_key Map.get(Ryal.payment_gateways(), :stripe)
@stripe_base "https://#{@stripe_api_key}:@api.stripe.com"

@spec create(atom, Ecto.Schema.t, String.t) :: {:ok, String.t}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to remove the need for an Ecto.Schema.t and use either multiple args or a map instead.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this level I agree. We shouldn't tie gateway usage to Ecto dependencies....Unless we are comitted to building on the Phoenix stack instead of a more general approach.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm good with sticking to Ecto. However, we don't really need Phoenix so much as we need Plug. To me, Ryal, ATM, will always require storage. It's up to the user if they want to mount the API. Ryal, however, would expect to be able to store data.

Unless, of course, we move the logic for Ryal into another dependency. But that's a longer-term goal. We need Ryal working first.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok let's ship it :shipit: and keep an eye on these dependencies as the code base grows.

def create(type, schema, stripe_base \\ @stripe_base)

def create(:credit_card, payment_method, stripe_base) do
credit_card_data = payment_method.proxy.data
customer_id = payment_method.user_id
|> PaymentGatewayQuery.get_external_id("stripe")
|> Ryal.repo.one!

credit_card_path = "/v1/customers/#{customer_id}/sources"
create_object credit_card_data, :credit_card, credit_card_path, stripe_base
end

def create(:customer, user, stripe_base) do
create_object user, :customer, "/v1/customers", stripe_base
end

defp create_object(schema, type, path, stripe_base) do
response = HTTPotion.post(stripe_base <> path, [body: params(type, schema)])

with {:ok, body} <- Poison.decode(response.body),
do: {:ok, body["id"]}
end

@spec update(atom, Ecto.Schema.t, String.t) :: {:ok, %{}}
def update(type, schema, stripe_base \\ @stripe_base)

@doc "Updates information on Stripe when the user data changes."
def update(:customer, payment_gateway, stripe_base) do
user = payment_gateway.user

response = stripe_base
<> "/v1/customers/#{payment_gateway.external_id}"
|> HTTPotion.post([body: params(:customer, user)])

Poison.decode(response.body)
end

@spec delete(atom, Ecto.Schema.t, String.t) :: {:ok, %{}}
def delete(atom, schema, stripe_base \\ @stripe_base)

@doc "Marks a customer account on Stripe as deleted."
def delete(:customer, payment_gateway, stripe_base) do
response = stripe_base
<> "/v1/customers/#{payment_gateway.external_id}"
|> HTTPotion.delete

Poison.decode(response.body)
end

defp params(:credit_card, credit_card) do
URI.encode_query %{
"source[object]" => "card",
"source[exp_month]" => credit_card.month,
"source[exp_year]" => credit_card.year,
"source[number]" => credit_card.number,
"source[cvc]" => credit_card.cvc,
"source[name]" => credit_card.name
}
end

defp params(:customer, user) do
URI.encode_query %{
email: user.email,
description: "Customer #{user.id} from Ryal."
}
end
end
10 changes: 6 additions & 4 deletions apps/ryal_core/lib/ryal/web/models/payment_method.ex
Expand Up @@ -37,11 +37,13 @@ defmodule Ryal.PaymentMethod do
schema "ryal_payment_methods" do
field :type, :string

embeds_one :data, Ryal.PaymentMethod.Proxy
embeds_one :proxy, Ryal.PaymentMethod.Proxy

has_many :payment_method_gateways, Ryal.PaymentMethodGateway

belongs_to :user, Ryal.user_module()

timestamps()
end

@required_fields ~w(type user_id)a
Expand All @@ -58,15 +60,15 @@ defmodule Ryal.PaymentMethod do
|> cast(set_module_type(params), @required_fields)
|> validate_required(@required_fields)
|> validate_inclusion(:type, @payment_method_types)
|> cast_embed(:data, required: true)
|> cast_embed(:proxy, required: true)
end

defp set_module_type(%{type: type} = params)
when type in @payment_method_types do
module_type = Macro.camelize(type)
{module_name, []} = Code.eval_string("Ryal.PaymentMethod.#{module_type}")
data = Map.get(params, :data, %{})
Map.put(params, :data, struct(module_name, data))
proxy_data = Map.get(params, :proxy, %{})
Map.put(params, :proxy, struct(module_name, proxy_data))
end

defp set_module_type(params), do: params
Expand Down
14 changes: 11 additions & 3 deletions apps/ryal_core/lib/ryal/web/models/payment_methods/proxy.ex
Expand Up @@ -10,6 +10,7 @@ defmodule Ryal.PaymentMethod.Proxy do
import Ecto.Changeset, only: [cast: 3]

embedded_schema do
field :data, :map
end

@doc """
Expand Down Expand Up @@ -38,8 +39,15 @@ defmodule Ryal.PaymentMethod.Proxy do
%module{} = struct
params = Map.from_struct(struct)

module
|> struct(%{})
|> module.changeset(params)
proxy_changeset = module
|> struct(%{})
|> module.changeset(params)

%__MODULE__{}
|> cast(%{data: params}, [:data])
|> Map.merge(%{
errors: proxy_changeset.errors,
valid?: proxy_changeset.valid?
})
end
end
18 changes: 18 additions & 0 deletions apps/ryal_core/lib/ryal/web/queries/payment_gateway_query.ex
@@ -0,0 +1,18 @@
defmodule Ryal.PaymentGatewayQuery do
@moduledoc "Queries for the `Ryal.PaymentGateway`."

use Ryal.Web, :query

alias Ryal.PaymentGateway

@doc """
Have a user id and want to get its external_id to one of the payment
gateways? This query should help you out fine.
"""
@spec get_external_id(integer, String.t) :: Ecto.Query.t
def get_external_id(user_id, gateway_type) do
from pg in PaymentGateway,
where: [user_id: ^user_id, type: ^gateway_type],
select: pg.external_id
end
end
Expand Up @@ -3,7 +3,8 @@ defmodule Ryal.Repo.Migrations.CreateRyalPaymentMethods do

def change do
create table(:ryal_payment_methods) do
add :data, :map, null: false
add :type, :string, null: false
add :proxy, :map, null: false

add :user_id, references(Ryal.user_table()), null: false

Expand Down
24 changes: 24 additions & 0 deletions apps/ryal_core/test/fixtures/stripe/credit_card.json
@@ -0,0 +1,24 @@
{
"id": "card_1AA3En2BZSQJcNSQ77orWzVS",
"object": "card",
"address_city": null,
"address_country": null,
"address_line1": null,
"address_line1_check": null,
"address_line2": null,
"address_state": null,
"address_zip": null,
"address_zip_check": null,
"brand": "Visa",
"country": "US",
"customer": "cus_AUIXSS9KUHRv5H",
"cvc_check": null,
"dynamic_last4": null,
"exp_month": 8,
"exp_year": 2018,
"funding": "credit",
"last4": "4242",
"metadata": {},
"name": null,
"tokenization_method": null
}