Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
CLOUDFRONT_DOMAIN=
S3_BUCKET=
SEGMENT_WRITE_KEY=
SENTRY_DSN=
STRIPE_SECRET_KEY=
STRIPE_PLATFORM_CLIENT_ID=
export AWS_ACCESS_KEY_ID=
export AWS_SECRET_ACCESS_KEY=
export CLOUDFRONT_DOMAIN=
export S3_BUCKET=
export SEGMENT_WRITE_KEY=
export SENTRY_DSN=
export STRIPE_SECRET_KEY=
export STRIPE_PLATFORM_CLIENT_ID=
23 changes: 16 additions & 7 deletions lib/code_corps/map_utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,21 @@ defmodule CodeCorps.MapUtils do
|> Map.delete(old_key)
end

def keys_to_string(map) do
for {key, val} <- map, into: %{} do
cond do
is_atom(key) -> {Atom.to_string(key), val}
true -> {key, val}
end
end
def keys_to_string(map), do: stringify_keys(map)

# Goes through a list and stringifies keys of any map member
def stringify_keys(nil), do: nil
def stringify_keys(%DateTime{} = val), do: val
def stringify_keys(map = %{}) do
map
|> Enum.map(fn {k, v} -> {stringify_key(k), stringify_keys(v)} end)
|> Enum.into(%{})
end
def stringify_keys([head | rest]), do: [stringify_keys(head) | stringify_keys(rest)]
# Default
def stringify_keys(not_a_map), do: not_a_map

def stringify_key(k) when is_atom(k), do: Atom.to_string(k)
def stringify_key(k), do: k

end
44 changes: 27 additions & 17 deletions lib/code_corps/stripe_service/adapters/stripe_connect_account.ex
Original file line number Diff line number Diff line change
@@ -1,16 +1,9 @@
defmodule CodeCorps.StripeService.Adapters.StripeConnectAccountAdapter do
import CodeCorps.MapUtils, only: [keys_to_string: 1]
import CodeCorps.StripeService.Util, only: [transform_map: 2]
import CodeCorps.StripeService.Util, only: [transform_attributes: 2, transform_map: 2]

@stripe_attributes [
:business_name, :business_url, :charges_enabled, :country, :default_currency, :details_submitted, :email, :id, :managed,
:support_email, :support_phone, :support_url, :transfers_enabled
]

@doc """
Mapping of stripe record attributes to locally stored attributes
Format is {:local_key, [:nesting, :of, :stripe, :keys]}
"""
# Mapping of stripe record attributes to locally stored attributes
# Format is {:local_key, [:nesting, :of, :stripe, :keys]}
@stripe_mapping [
{:id_from_stripe, [:id]},
{:business_name, [:business_name]},
Expand Down Expand Up @@ -45,7 +38,9 @@ defmodule CodeCorps.StripeService.Adapters.StripeConnectAccountAdapter do
{:legal_entity_personal_address_postal_code, [:legal_entity, :personal_address, :postal_code]},
{:legal_entity_personal_address_state, [:legal_entity, :personal_address, :state]},
{:legal_entity_phone_number, [:legal_entity, :phone_number]},
{:legal_entity_personal_id_number, [:legal_entity, :personal_id_number]},
{:legal_entity_personal_id_number_provided, [:legal_entity, :personal_id_number_provided]},
{:legal_entity_ssn_last_4, [:legal_entity, :ssn_last_4]},
{:legal_entity_ssn_last_4_provided, [:legal_entity, :ssn_last_4_provided]},
{:legal_entity_type, [:legal_entity, :type]},
{:legal_entity_verification_details, [:legal_entity, :verification, :details]},
Expand All @@ -62,6 +57,19 @@ defmodule CodeCorps.StripeService.Adapters.StripeConnectAccountAdapter do
{:verification_fields_needed, [:verification, :fields_needed]}
]

@doc """
Transforms a set of local attributes into a map of parameters used to
update a `%Stripe.Account{}`.
"""
def from_params(%{} = attributes) do
result =
attributes
|> remove_attributes()
|> transform_attributes(@stripe_mapping)

{:ok, result}
end

@doc """
Transforms a `%Stripe.Account{}` and a set of local attributes into a
map of parameters used to create or update a `StripeConnectAccount` record.
Expand Down Expand Up @@ -89,21 +97,23 @@ defmodule CodeCorps.StripeService.Adapters.StripeConnectAccountAdapter do
end

defp get_non_stripe_attributes(%{} = attributes) do
attributes
|> Map.take(@non_stripe_attributes)
attributes |> Map.take(@non_stripe_attributes)
end

defp add_to(%{} = attributes, %{} = params) do
params
|> Map.merge(attributes)
params |> Map.merge(attributes)
end

defp add_nested_attributes(map, stripe_account) do
map
|> add_external_account(stripe_account)
end
map |> add_external_account(stripe_account)
end

defp add_external_account(map, %Stripe.Account{external_accounts: %{data: []}}), do: map
defp add_external_account(map, %Stripe.Account{external_accounts: %{data: [head | _]}}), do: map |> do_add_external_account(head)
defp do_add_external_account(map, %{"id" => id}), do: map |> Map.put(:external_account, id)

defp remove_attributes(%{"legal_entity_verification_status" => "verified"} = attributes) do
attributes |> Map.delete("legal_entity_verification_document")
end
defp remove_attributes(attributes), do: attributes
end
52 changes: 12 additions & 40 deletions lib/code_corps/stripe_service/stripe_connect_account.ex
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
defmodule CodeCorps.StripeService.StripeConnectAccountService do
alias CodeCorps.{Repo, StripeConnectAccount, StripeFileUpload}
alias CodeCorps.StripeService.Adapters.{StripeConnectAccountAdapter, StripeFileUploadAdapter}

alias Ecto.Multi
alias CodeCorps.{Repo, StripeConnectAccount}
alias CodeCorps.StripeService.Adapters.{StripeConnectAccountAdapter}

@api Application.get_env(:code_corps, :stripe)

Expand All @@ -16,43 +14,17 @@ defmodule CodeCorps.StripeService.StripeConnectAccountService do
end
end

def add_external_account(%StripeConnectAccount{id_from_stripe: stripe_id} = record, external_account) do
with {:ok, %Stripe.Account{} = stripe_account} <- @api.Account.update(stripe_id, %{external_account: external_account}),
{:ok, params} <- StripeConnectAccountAdapter.to_params(stripe_account, %{})
do
record
|> StripeConnectAccount.webhook_update_changeset(params)
|> Repo.update
end
end

def add_vertification_document(
%StripeConnectAccount{id_from_stripe: account_id} = record,
%{"legal_entity_verification_document" => document} = attributes
) do
with {:ok, %Stripe.FileUpload{} = stripe_file_upload} <- Stripe.FileUpload.retrieve(document),
{:ok, %Stripe.Account{} = stripe_account} <- Stripe.Account.update(account_id, %{legal_entity: %{verification: %{document: document}}}),
{:ok, file_upload_params} <- StripeFileUploadAdapter.to_params(stripe_file_upload, attributes),
{:ok, connect_account_params} <- StripeConnectAccountAdapter.to_params(stripe_account, attributes),
account_changeset <- record |> StripeConnectAccount.webhook_update_changeset(connect_account_params),
file_changeset <- %StripeFileUpload{} |> StripeFileUpload.create_changeset(file_upload_params)
def update(%StripeConnectAccount{id_from_stripe: id_from_stripe} = account, %{} = attributes) do
with {:ok, from_params} <- StripeConnectAccountAdapter.from_params(attributes),
{:ok, %Stripe.Account{} = stripe_account} <- @api.Account.update(id_from_stripe, from_params),
{:ok, params} <- StripeConnectAccountAdapter.to_params(stripe_account, attributes),
{:ok, %StripeConnectAccount{} = updated_account} <- account |> StripeConnectAccount.webhook_update_changeset(params) |> Repo.update
do
multi =
Multi.new
|> Multi.update(:stripe_connect_account, account_changeset)
|> Multi.insert(:stripe_file_upload, file_changeset)

case Repo.transaction(multi) do
{:ok, %{stripe_connect_account: account, stripe_file_upload: _}} ->
{:ok, account}
{:error, :stripe_connect_account, %Ecto.Changeset{} = account_changeset, %{}} ->
{:error, account_changeset}
# If creating the file failed due to validation, we add a generic error
# to the account changeset and return that to be rendered.
{:error, :stripe_file_upload, %Ecto.Changeset{} = _, _} ->
account_changeset |> Ecto.Changeset.add_error(:legal_entity_verification_document, "is invalid")
{:error, account_changeset}
end
{:ok, updated_account}
else
{:error, %Ecto.Changeset{} = changeset} -> {:error, changeset}
{:error, %Stripe.APIErrorResponse{} = error} -> {:error, error}
_ -> {:error, :unhandled}
Copy link
Contributor

Choose a reason for hiding this comment

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

We will likely want to sweep through the code at some point and remove these _ -> unhandled cases. They basically swallow any unexpected errors in the process. We should explicitly list any failures we support and handle. If anything unexpected fails, we should treat it as a bug.

This is basically equivalent with us wrapping a huge chunk of code into a try-catch that catches any sort of exception.

end
end
end
7 changes: 4 additions & 3 deletions lib/code_corps/stripe_service/stripe_connect_plan.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ defmodule CodeCorps.StripeService.StripeConnectPlanService do

def create(%{"project_id" => project_id} = attributes) do
with {:ok, %Project{} = project} <- get_project(project_id) |> ProjectCanEnableDonations.validate,
%{} = create_attributes <- get_create_attributes(),
%{} = create_attributes <- get_create_attributes(project_id),
connect_account_id <- project.organization.stripe_connect_account.id_from_stripe,
{:ok, plan} <- @api.Plan.create(create_attributes, connect_account: connect_account_id),
{:ok, params} <- StripeConnectPlanAdapter.to_params(plan, attributes)
Expand All @@ -22,11 +22,12 @@ defmodule CodeCorps.StripeService.StripeConnectPlanService do
end
end

defp get_create_attributes do
@spec get_create_attributes(binary) :: map
defp get_create_attributes(project_id) do
%{
amount: 1, # in cents
currency: "usd",
id: "month",
id: "month_project_" <> to_string(project_id),
Copy link
Contributor

Choose a reason for hiding this comment

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

Did you mean month_plan_ here or is _project_ intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@begedin project is intentional since plans are per-project.

interval: "month",
name: "Monthly donation",
statement_descriptor: "CODECORPS.ORG Donation" # No more than 22 chars
Expand Down
47 changes: 47 additions & 0 deletions lib/code_corps/stripe_service/util.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,53 @@ defmodule CodeCorps.StripeService.Util do
"""
def transform_map(api_map, mapping), do: mapping |> Enum.reduce(%{}, &map_field(&1, &2, api_map))

def transform_attributes(attributes, mapping) do
attributes_map = map_keys_to_atoms(attributes)
mapping |> Enum.reduce(%{}, &map_attribute(&1, &2, attributes_map))
end
Copy link
Contributor

Choose a reason for hiding this comment

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

Might not be a bad Idea to have a unit test for this specifically. Separate issue might be good enough for now, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍 for separate issue.


defp map_keys_to_atoms(m) do
result = Enum.map(m, fn
{k, v} when is_binary(k) ->
a = String.to_existing_atom(k)
{a, v}
entry ->
entry
end)
result |> Enum.into(%{})
end

defp map_attribute({source_field, target_path}, target_map, source_map) do
value = source_map |> Map.get(source_field)
list = target_path |> Enum.reverse
result = put_value(list, value, %{})
deep_merge(target_map, result)
end

defp put_value(_, value, map) when is_nil(value), do: map
defp put_value([head | tail], value, map) do
new_value = Map.put(%{}, head, value)
put_value(tail, new_value, map)
end
defp put_value([], new_value, _map), do: new_value

defp deep_merge(left, right) do
Map.merge(left, right, &deep_resolve/3)
end

# Key exists in both maps, and both values are maps as well.
# These can be merged recursively.
defp deep_resolve(_key, left = %{}, right = %{}) do
deep_merge(left, right)
end

# Key exists in both maps, but at least one of the values is
# NOT a map. We fall back to standard merge behavior, preferring
# the value on the right.
defp deep_resolve(_key, _left, right) do
right
end

# Takes a tuple which contains a target field and a source path,
# then puts value on the source path from the source map
# into to target map under the target field name.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ defmodule CodeCorps.StripeService.WebhookProcessing.WebhookProcessor do
end

defp handle_event(json, event, handler) do
case handler.handle_event(json) |> Tuple.to_list do
case json |> handler.handle_event |> Tuple.to_list do
[:ok, :unhandled_event] -> event |> set_unhandled
[:ok | _results] -> event |> set_processed
[:error | _error] -> event |> set_errored
Expand Down
9 changes: 5 additions & 4 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,15 @@ defmodule CodeCorps.Mixfile do
{:phoenix_live_reload, "~> 1.0", only: :dev},
{:gettext, "~> 0.12"},
{:cowboy, "~> 1.0"},
{:arc, git: "https://github.com/stavro/arc.git", ref: "354d4d2e1b86bcd6285db3528118fe3f5db36cf5", override: true}, # Photo uploads
{:arc_ecto, "~> 0.4.4"},
{:arc, "~> 0.6"}, # Photo uploads
{:arc_ecto, "~> 0.5"},
{:benchfella, "~> 0.3.0", only: :dev},
{:canary, "~> 1.1"}, # Authorization
{:comeonin, "~> 2.0"},
{:corsica, "~> 0.4"}, # CORS
{:credo, "~> 0.5", only: [:dev, :test]}, # Code style suggestions
{:earmark, "~> 1.0"}, # Markdown rendering
{:ex_aws, "~> 0.4"}, # Amazon AWS
{:ex_aws, "~> 1.0"}, # Amazon AWS
{:excoveralls, "~> 0.5", only: :test}, # Test coverage
{:ex_doc, "~> 0.14", only: [:dev, :test]},
{:ex_machina, "~> 1.0", only: :test}, # test factories
Expand All @@ -87,9 +87,10 @@ defmodule CodeCorps.Mixfile do
{:mix_test_watch, "~> 0.2", only: :dev}, # Test watcher
{:poison, "~> 2.0"},
{:scrivener_ecto, "~> 1.0"}, # DB query pagination
{:segment, github: "stueccles/analytics-elixir"}, # Segment analytics
{:segment, "~> 0.1"}, # Segment analytics
{:sentry, "~> 2.0"}, # Sentry error tracking
{:stripity_stripe, git: "https://github.com/code-corps/stripity_stripe.git", branch: "2.0"}, # Stripe
{:sweet_xml, "~> 0.5"},
{:timber, "~> 0.4"}, # Logging
{:timex, "~> 3.0"},
{:timex_ecto, "~> 3.0"},
Expand Down
Loading