Skip to content

Commit

Permalink
Merge pull request #653 from code-corps/649-track-charge-succeeded-ev…
Browse files Browse the repository at this point in the history
…ent-on-connect

Add stripe charge model and event tracking
  • Loading branch information
joshsmith committed Jan 21, 2017
2 parents 2db90e6 + 0026355 commit 0527a35
Show file tree
Hide file tree
Showing 21 changed files with 601 additions and 41 deletions.
36 changes: 23 additions & 13 deletions lib/code_corps/analytics/segment.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,18 @@ defmodule CodeCorps.Analytics.Segment do
```
"""

alias CodeCorps.{Comment, OrganizationMembership, StripeInvoice, Task, User, UserCategory, UserRole, UserSkill}
alias CodeCorps.{
Comment,
OrganizationMembership,
StripeConnectCharge,
StripeInvoice,
Task,
User,
UserCategory,
UserRole,
UserSkill
}

alias Ecto.Changeset

@api Application.get_env(:code_corps, :analytics)
Expand All @@ -26,9 +37,7 @@ defmodule CodeCorps.Analytics.Segment do
Uses the action on the record to determine the event name that should be passed in for the `track` call.
"""
@spec get_event_name(atom, struct) :: String.t
def get_event_name(action, _) when action in @actions_without_properties do
friendly_action_name(action)
end
def get_event_name(action, _) when action in @actions_without_properties, do: friendly_action_name(action)
def get_event_name(:created, %OrganizationMembership{}), do: "Requested Organization Membership"
def get_event_name(:edited, %OrganizationMembership{}), do: "Approved Organization Membership"
def get_event_name(:payment_succeeded, %StripeInvoice{}), do: "Processed Subscription Payment"
Expand Down Expand Up @@ -63,13 +72,15 @@ defmodule CodeCorps.Analytics.Segment do
do_track(conn, action_name, properties(record))
{:ok, record}
end
def track({:ok, %{user_id: user_id} = record}, action, nil) do
def track({:error, %Changeset{} = changeset}, _action, _conn), do: {:error, changeset}

def track({:error, errors}, :deleted, _conn), do: {:error, errors}

def track({:ok, %{user_id: user_id} = record}, action) do
action_name = get_event_name(action, record)
do_track(user_id, action_name, properties(record))
{:ok, record}
end
def track({:error, %Changeset{} = changeset}, _action, _conn), do: {:error, changeset}
def track({:error, errors}, :deleted, _conn), do: {:error, errors}

@doc """
Calls `track` with the "Signed In" event in the configured API module.
Expand Down Expand Up @@ -122,15 +133,15 @@ defmodule CodeCorps.Analytics.Segment do
organization_id: organization_membership.organization.id
}
end
defp properties(invoice = %StripeInvoice{}) do
revenue = invoice.total / 100 # TODO: this only works for some currencies
currency = String.capitalize(invoice.currency) # ISO 4127 format
defp properties(charge = %StripeConnectCharge{}) do
revenue = charge.amount / 100 # TODO: this only works for some currencies
currency = String.capitalize(charge.currency) # ISO 4127 format

%{
charge_id: charge.id,
currency: currency,
invoice_id: invoice.id,
revenue: revenue,
user_id: invoice.user_id
user_id: charge.user_id
}
end
defp properties(task = %Task{}) do
Expand Down Expand Up @@ -167,7 +178,6 @@ defmodule CodeCorps.Analytics.Segment do
%{}
end


defp traits(user) do
%{
admin: user.admin,
Expand Down
64 changes: 64 additions & 0 deletions lib/code_corps/stripe_service/adapters/stripe_connect_charge.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
defmodule CodeCorps.StripeService.Adapters.StripeConnectChargeAdapter do

alias CodeCorps.{
StripeConnectAccount, StripeConnectCustomer, Repo
}

alias CodeCorps.StripeService.Util

# Mapping of stripe record attributes to locally stored attributes
# Format is {:local_key, [:nesting, :of, :stripe, :keys]}
#
# TODO:
#
# Relationships we have
# * customer, user
# Relationship we store but do not define
# * application, invoice, connect account, balance transaction, source transfer, transfer
@stripe_mapping [
{:amount, [:amount]},
{:amount_refunded, [:amount_refunded]},
{:application_id_from_stripe, [:application]},
{:application_fee_id_from_stripe, [:application_fee]},
{:balance_transaction_id_from_stripe, [:balance_transaction]},
{:captured, [:captured]},
{:created, [:created]},
{:currency, [:currency]},
{:customer_id_from_stripe, [:customer]},
{:description, [:description]},
{:failure_code, [:failure_code]},
{:failure_message, [:failure_message]},
{:id_from_stripe, [:id]},
{:invoice_id_from_stripe, [:invoice]},
{:paid, [:paid]},
{:refunded, [:refunded]},
{:review_id_from_stripe, [:review]},
{:source_transfer_id_from_stripe, [:source_transfer]},
{:statement_descriptor, [:statement_descriptor]},
{:status, [:status]}
]

@doc """
Transforms a `%Stripe.Charge{}` and a set of local attributes into a
map of parameters used to create or update a `StripeConnectCharge` record.
"""
def to_params(%Stripe.Charge{} = stripe_charge, %StripeConnectAccount{id: connect_account_id}) do
result =
stripe_charge
|> Map.from_struct
|> Util.transform_map(@stripe_mapping)
|> Map.put(:stripe_connect_account_id, connect_account_id)
|> add_other_associations()

{:ok, result}
end

defp add_other_associations(%{customer_id_from_stripe: customer_id_from_stripe} = attributes) do
%StripeConnectCustomer{id: customer_id, user_id: user_id} =
Repo.get_by(StripeConnectCustomer, id_from_stripe: customer_id_from_stripe)

attributes
|> Map.put(:user_id, user_id)
|> Map.put(:stripe_connect_customer_id, customer_id)
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
defmodule CodeCorps.StripeService.Events.ConnectChargeSucceeded do
def handle(%{data: %{object: %{id: id_from_stripe}}, user_id: connect_account_id_from_stripe}) do
CodeCorps.StripeService.StripeConnectChargeService.create(id_from_stripe, connect_account_id_from_stripe)
end
end
20 changes: 20 additions & 0 deletions lib/code_corps/stripe_service/stripe_connect_charge_service.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
defmodule CodeCorps.StripeService.StripeConnectChargeService do
alias CodeCorps.{
Repo, StripeConnectAccount, StripeConnectCharge
}
alias CodeCorps.StripeService.Adapters.StripeConnectChargeAdapter

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

def create(id_from_stripe, connect_account_id_from_stripe) do
with %StripeConnectAccount{} = stripe_connect_account <- Repo.get_by(StripeConnectAccount, id_from_stripe: connect_account_id_from_stripe),
{:ok, %Stripe.Charge{} = api_charge} <- @api.Charge.retrieve(id_from_stripe, connect_account: connect_account_id_from_stripe),
{:ok, params} = StripeConnectChargeAdapter.to_params(api_charge, stripe_connect_account)
do
%StripeConnectCharge{}
|> StripeConnectCharge.create_changeset(params)
|> Repo.insert
|> CodeCorps.Analytics.Segment.track(:created)
end
end
end
1 change: 0 additions & 1 deletion lib/code_corps/stripe_service/stripe_invoice_service.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ defmodule CodeCorps.StripeService.StripeInvoiceService do
%StripeInvoice{}
|> StripeInvoice.create_changeset(params)
|> Repo.insert
|> CodeCorps.Analytics.Segment.track(:payment_succeeded, nil)
else
failure -> failure
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ defmodule CodeCorps.StripeService.WebhookProcessing.ConnectEventHandler do
def handle_event(%{type: type} = attributes), do: do_handle(type, attributes)

defp do_handle("account.updated", attributes), do: Events.AccountUpdated.handle(attributes)
defp do_handle("charge.succeeded", attributes), do: Events.ConnectChargeSucceeded.handle(attributes)
defp do_handle("customer.subscription.deleted", attributes), do: Events.CustomerSubscriptionDeleted.handle(attributes)
defp do_handle("customer.subscription.updated", attributes), do: Events.CustomerSubscriptionUpdated.handle(attributes)
defp do_handle("invoice.payment_succeeded", attributes), do: Events.InvoicePaymentSucceeded.handle(attributes)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,13 @@ defmodule CodeCorps.StripeService.WebhookProcessing.EventHandler do
end
end

defp call_ignored_handler(local_event, handler), do: IgnoredEventHandler.handle(local_event, handler)
defp call_ignored_handler(%StripeEvent{} = local_event, handler), do: IgnoredEventHandler.handle(local_event, handler)

defp call_handler(api_event, local_event, handler) do
defp call_handler(%Stripe.Event{} = api_event, %StripeEvent{} = local_event, handler) do
# results are multiple, so we convert the tuple to list for easier matching
case api_event |> handler.handle_event |> Tuple.to_list do
user_id = Map.get(local_event, :user_id)
event = api_event |> Map.merge(%{user_id: user_id})
case event |> handler.handle_event |> Tuple.to_list do
[:ok, :unhandled_event] -> local_event |> set_unhandled
[:ok | _results] -> local_event |> set_processed
[:error | _error] -> local_event |> set_errored
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ defmodule CodeCorps.StripeService.WebhookProcessing.WebhookProcessor do
with user_id <- event_params |> Map.get("user_id"),
{:ok, %Stripe.Event{} = api_event} <- retrieve_event_from_api(id, user_id)
do
EventHandler.handle(api_event, handler)
EventHandler.handle(api_event, handler, user_id)
end
end

Expand Down
7 changes: 7 additions & 0 deletions lib/code_corps/stripe_testing/charge.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
defmodule CodeCorps.StripeTesting.Charge do
import CodeCorps.StripeTesting.Helpers

def retrieve(_id, _opts) do
{:ok, load_fixture(Stripe.Charge, "charge")}
end
end
66 changes: 66 additions & 0 deletions lib/code_corps/stripe_testing/fixtures/charge.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"id": "charge",
"object": "charge",
"amount": 100,
"amount_refunded": 0,
"application": null,
"application_fee": null,
"balance_transaction": "test_balance_transaction_for_charge",
"captured": true,
"created": 1484869309,
"currency": "usd",
"customer": "test_customer_for_charge",
"description": "Test Charge (created for fixture)",
"destination": null,
"dispute": null,
"failure_code": null,
"failure_message": null,
"fraud_details": {},
"invoice": null,
"livemode": false,
"metadata": {
},
"order": null,
"outcome": null,
"paid": true,
"receipt_email": null,
"receipt_number": null,
"refunded": false,
"refunds": {
"object": "list",
"data": [],
"has_more": false,
"total_count": 0,
"url": "/v1/charges/ch_123/refunds"
},
"review": null,
"shipping": null,
"source": {
"id": "card_123",
"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": null,
"cvc_check": null,
"dynamic_last4": null,
"exp_month": 8,
"exp_year": 2018,
"funding": "credit",
"last4": "4242",
"metadata": {
},
"name": null,
"tokenization_method": null
},
"source_transfer": null,
"statement_descriptor": null,
"status": "succeeded"
}
1 change: 1 addition & 0 deletions lib/code_corps/stripe_testing/helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ defmodule CodeCorps.StripeTesting.Helpers do
Load a stripe response fixture through stripity_stripe, into a
stripity_stripe struct
"""
@spec load_fixture(module, String.t) :: struct
def load_fixture(module, id) do
fixture_map = id |> build_file_path |> File.read! |> Poison.decode!
Stripe.Converter.stripe_map_to_struct(module, fixture_map)
Expand Down
2 changes: 1 addition & 1 deletion mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
"segment": {:hex, :segment, "0.1.1", "47bf9191590e7a533c105d1e21518e0d6da47c91e8d98ebb649c624db5dfc359", [:mix], [{:httpoison, "~> 0.8", [hex: :httpoison, optional: false]}, {:poison, "~> 1.3 or ~> 2.0", [hex: :poison, optional: false]}]},
"sentry": {:hex, :sentry, "2.1.0", "51e7ca261b519294ac73b30763893c4a7ad2005205514aefa5bf37ccb83e44ea", [:mix], [{:hackney, "~> 1.6.1", [hex: :hackney, optional: false]}, {:plug, "~> 1.0", [hex: :plug, optional: true]}, {:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, optional: false]}, {:uuid, "~> 1.0", [hex: :uuid, optional: false]}]},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.0", "edee20847c42e379bf91261db474ffbe373f8acb56e9079acb6038d4e0bf414f", [:make, :rebar], []},
"stripity_stripe": {:git, "https://github.com/code-corps/stripity_stripe.git", "2ebf493fb925865455a552a828331884944e864b", [branch: "2.0"]},
"stripity_stripe": {:git, "https://github.com/code-corps/stripity_stripe.git", "adb86034f308d756d307725f7ff67603cebefe6a", [branch: "2.0"]},
"sweet_xml": {:hex, :sweet_xml, "0.6.4", "5d613e63033b1d6362898f3f1d65f70a352b32d4b32e1367bb20b8c2e30ba79d", [:mix], []},
"timber": {:hex, :timber, "0.4.7", "df3fcd79bcb4eb4b53874d906ef5f3a212937b4bc7b7c5b244745202cc389443", [:mix], [{:ecto, "~> 2.0", [hex: :ecto, optional: true]}, {:phoenix, "~> 1.2", [hex: :phoenix, optional: true]}, {:plug, "~> 1.2", [hex: :plug, optional: true]}, {:poison, "~> 2.0 or ~> 3.0", [hex: :poison, optional: false]}]},
"timex": {:hex, :timex, "3.1.7", "71f9c32e13ff4860e86a314303757cc02b3ead5db6e977579a2935225ce9a666", [:mix], [{:combine, "~> 0.7", [hex: :combine, optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, optional: false]}]},
Expand Down
36 changes: 36 additions & 0 deletions priv/repo/migrations/20170121014100_add_stripe_connect_charge.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
defmodule CodeCorps.Repo.Migrations.AddStripeConnectCharge do
use Ecto.Migration

def change do
create table(:stripe_connect_charges) do
add :amount, :integer
add :amount_refunded, :integer
add :application_id_from_stripe, :string
add :application_fee_id_from_stripe, :string
add :balance_transaction_id_from_stripe, :string
add :captured, :boolean
add :created, :integer
add :currency, :string
add :customer_id_from_stripe, :string
add :description, :string
add :failure_code, :string
add :failure_message, :string
add :id_from_stripe, :string
add :invoice_id_from_stripe, :string
add :paid, :boolean
add :refunded, :boolean
add :review_id_from_stripe, :string
add :source_transfer_id_from_stripe, :string
add :statement_descriptor, :string
add :status, :string

add :stripe_connect_account_id, references(:stripe_connect_accounts)
add :stripe_connect_customer_id, references(:stripe_connect_customers), null: false
add :user_id, references(:users), null: false

timestamps()
end

create index(:stripe_connect_charges, [:id_from_stripe], unique: true)
end
end
Loading

0 comments on commit 0527a35

Please sign in to comment.