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
2 changes: 1 addition & 1 deletion lib/code_corps/stripe_service/events/account_updated.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
defmodule CodeCorps.StripeService.Events.AccountUpdated do
def handle(%{"data" => %{"object" => %{"id" => id_from_stripe}}}) do
def handle(%{data: %{object: %{id: id_from_stripe}}}) do

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

One of the changes involved passing in the fetched Stripe.Event into the handlers, instead of the raw json.

CodeCorps.StripeService.StripeConnectAccountService.update_from_stripe(id_from_stripe)
end
end
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
defmodule CodeCorps.StripeService.Events.ConnectExternalAccountCreated do
def handle(%{"data" => %{"object" => %{"account" => account_id_from_stripe, "id" => id_from_stripe}}}) do
def handle(%{data: %{object: %{account: account_id_from_stripe, id: id_from_stripe}}}) do
CodeCorps.StripeService.StripeConnectExternalAccountService.create(id_from_stripe, account_id_from_stripe)
end
end
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule CodeCorps.StripeService.Events.CustomerSourceUpdated do
def handle(%{"data" => %{"object" => %{"id" => card_id, "object" => "card"}}}) do
def handle(%{data: %{object: %Stripe.Card{id: card_id}}}) do
CodeCorps.StripeService.StripePlatformCardService.update_from_stripe(card_id)
end

def handle(%{"data" => %{"object" => %{"id" => _, "object" => _}}}), do: {:error, :unsupported_object}
def handle(_data), do: {:error, :unsupported_object}
end
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
defmodule CodeCorps.StripeService.Events.CustomerSubscriptionDeleted do
def handle(%{"data" => %{"object" => %{"id" => stripe_sub_id, "customer" => connect_customer_id}}}) do
def handle(%{data: %{object: %{id: stripe_sub_id, customer: connect_customer_id}}}) do
CodeCorps.StripeService.StripeConnectSubscriptionService.update_from_stripe(stripe_sub_id, connect_customer_id)
end
end
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
defmodule CodeCorps.StripeService.Events.CustomerSubscriptionUpdated do
def handle(%{"data" => %{"object" => %{"id" => stripe_sub_id, "customer" => connect_customer_id}}}) do
def handle(%{data: %{object: %{id: stripe_sub_id, customer: connect_customer_id}}}) do
CodeCorps.StripeService.StripeConnectSubscriptionService.update_from_stripe(stripe_sub_id, connect_customer_id)
end
end
2 changes: 1 addition & 1 deletion lib/code_corps/stripe_service/events/customer_updated.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
defmodule CodeCorps.StripeService.Events.CustomerUpdated do
def handle(%{"data" => %{"object" => %{"id" => id_from_stripe}}}) do
def handle(%{data: %{object: %{id: id_from_stripe}}}) do
CodeCorps.StripeService.StripePlatformCustomerService.update_from_stripe(id_from_stripe)
end
end
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
defmodule CodeCorps.StripeService.Events.InvoicePaymentSucceeded do
def handle(%{"data" => %{"object" => %{"id" => id_from_stripe, "customer" => customer_id_from_stripe}}}) do
def handle(%{data: %{object:
%{id: id_from_stripe, customer: customer_id_from_stripe}
}}) do
CodeCorps.StripeService.StripeInvoiceService.create(id_from_stripe, customer_id_from_stripe)
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ defmodule CodeCorps.StripeService.WebhookProcessing.ConnectEventHandler do
in which the first member is `:ok`, followed by one or more other elements, usually modified records.
* `{:ok, :unhandled_event}` if the specific event is not supported yet or at all
"""
def handle_event(%{"type" => type} = attributes), do: do_handle(type, attributes)
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("account.external_account.created", attributes), do: Events.ConnectExternalAccountCreated.handle(attributes)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
defmodule CodeCorps.StripeService.WebhookProcessing.EnvironmentFilter do
@moduledoc """
Used to filter events based on environment
"""

@doc """
Returns true if the livemode attribute of the event matches
the current environment of the Phoenix application.

- livemode events are processed only in production.
- non-livemode events are processed in all other environments
"""
def environment_matches?(%{"livemode" => livemode}) do
case Application.get_env(:code_corps, :stripe_env) do
:prod -> livemode
_ -> !livemode
end
end
end
64 changes: 64 additions & 0 deletions lib/code_corps/stripe_service/webhook_processing/event_handler.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
defmodule CodeCorps.StripeService.WebhookProcessing.EventHandler do
alias CodeCorps.{StripeEvent, Repo}
alias CodeCorps.StripeService.Adapters.StripeEventAdapter
alias CodeCorps.StripeService.WebhookProcessing.{
ConnectEventHandler, IgnoredEventHandler, PlatformEventHandler
}

def handle(%Stripe.Event{type: type} = api_event, handler) do
with {:ok, endpoint} <- infer_endpoint_from_handler(handler),
{:ok, %StripeEvent{} = local_event} <- find_or_create_event(api_event, endpoint)
do
case IgnoredEventHandler.should_handle?(type) do
true -> call_ignored_handler(local_event)
false -> call_handler(api_event, local_event, handler)
end
else
failure -> failure
end
end

defp infer_endpoint_from_handler(ConnectEventHandler), do: {:ok, "connect"}
defp infer_endpoint_from_handler(PlatformEventHandler), do: {:ok, "platform"}

defp find_or_create_event(%Stripe.Event{} = api_event, endpoint) do
case find_event(api_event.id) do
%StripeEvent{status: "processing"} -> {:error, :already_processing}
%StripeEvent{} = local_event -> {:ok, local_event}
nil -> create_event(api_event, endpoint)
end
end

defp find_event(id_from_stripe) do
Repo.get_by(StripeEvent, id_from_stripe: id_from_stripe)
end

defp create_event(%Stripe.Event{} = api_event, endpoint) do
with {:ok, params} <- StripeEventAdapter.to_params(api_event, %{"endpoint" => endpoint}) do
%StripeEvent{} |> StripeEvent.create_changeset(params) |> Repo.insert
end
end

defp call_ignored_handler(local_event), do: IgnoredEventHandler.handle(local_event)

defp call_handler(api_event, 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
[:ok, :unhandled_event] -> local_event |> set_unhandled
[:ok | _results] -> local_event |> set_processed
[:error | _error] -> local_event |> set_errored
end
end

defp set_errored(%StripeEvent{} = local_event) do
local_event |> StripeEvent.update_changeset(%{status: "errored"}) |> Repo.update
end

defp set_processed(%StripeEvent{} = local_event) do
local_event |> StripeEvent.update_changeset(%{status: "processed"}) |> Repo.update
end

defp set_unhandled(%StripeEvent{} = local_event) do
local_event |> StripeEvent.update_changeset(%{status: "unhandled"}) |> Repo.update
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
defmodule CodeCorps.StripeService.WebhookProcessing.IgnoredEventHandler do
alias CodeCorps.{StripeEvent, Repo}

@ignored_event_types [
"application_fee.created",
"customer.created",
"customer.source.created",
"customer.subscription.created",
"invoice.created",
"plan.created"
]

@doc """
Determines if an event type should be handled by __MODULE__

Returns true or false depending on specified type
"""
@spec should_handle?(String.t) :: boolean
def should_handle?(type), do: Enum.member?(ignored_event_types, type)

@doc """
Returns a list of event types which are being explicitly ignored by the application.
"""
@spec ignored_event_types :: list
def ignored_event_types, do: @ignored_event_types

@doc """
Takes in a `CodeCorps.StripeEvent` to be processed as "ignored".
Determines the reason for ignoring the event, then updates the record to
`status: "ignored"` and `ignored_reason: inferred_message`

Returns `{:ok, %CodeCorps.StripeEvent{}}
"""
@spec handle(StripeEvent.t) :: {:ok, StripeEvent.t}
def handle(%StripeEvent{type: type} = local_event) do
with ignored_reason <- get_reason(type) do
local_event |> set_ignored(ignored_reason)
end
end

@spec get_reason(String.t) :: String.t
defp get_reason("application_fee.created"), do: "We don't make use of the application fee object."
defp get_reason("customer.created"), do: "Customers are only created from the client."
defp get_reason("customer.source.created"), do: "Cards are only created from the client. No need to handle"
defp get_reason("customer.subscription.created"), do: "Subscriptions are only created from the client."
defp get_reason("invoice.created"), do: "We prefer to handle other lifecycle events for invoices, like payment_succeeded."
defp get_reason("plan.created"), do: "Plans are only created from the client."

@spec set_ignored(StripeEvent.t, String.t) :: {:ok, StripeEvent.t}
defp set_ignored(%StripeEvent{} = local_event, ignored_reason) do
local_event
|> StripeEvent.update_changeset(%{status: "ignored", ignored_reason: ignored_reason})
|> Repo.update
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ defmodule CodeCorps.StripeService.WebhookProcessing.PlatformEventHandler do
in which the first member is `:ok`, followed by one or more other elements, usually modified records.
* `{:ok, :unhandled_event}` if the specific event is not supported yet or at all
"""
def handle_event(%{"type" => type} = attributes), do: do_handle(type, attributes)
def handle_event(%{type: type} = attributes), do: do_handle(type, attributes)

defp do_handle("customer.updated", attributes), do: Events.CustomerUpdated.handle(attributes)
defp do_handle("customer.source.updated", attributes), do: Events.CustomerSourceUpdated.handle(attributes)
Expand Down
104 changes: 21 additions & 83 deletions lib/code_corps/stripe_service/webhook_processing/webhook_processor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,109 +3,47 @@ defmodule CodeCorps.StripeService.WebhookProcessing.WebhookProcessor do
Used to process a Stripe webhook request.
"""

alias CodeCorps.StripeEvent
alias CodeCorps.Repo
alias CodeCorps.StripeService.WebhookProcessing.{ConnectEventHandler, PlatformEventHandler}
alias CodeCorps.StripeService.Adapters.StripeEventAdapter
alias CodeCorps.StripeService.WebhookProcessing.EventHandler

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

@doc """
Used to process a Stripe webhook event.
Used to process a Stripe webhook event in an async manner.

Receives the event JSON as the first parameter.

Since a webhook can be a platform or a connect webhook, the function requires
the handler module as the second parameter.

## Returns

- `{:ok, pid}` if the event will be handled
- `{:error, :ignored_by_environment}` if the event was ignored due to
environment mismatch

## Note

Stripe events can have their `livemode` property set to `true` or `false`.
A livemode `true` event should be handled by the production environment,
while all other environments handle livemode `false` events.
Returns `{:ok, pid}`
"""
def process_async(%{"id" => id, "livemode" => livemode, "user_id" => user_id} = json, handler) do
case event_matches_environment?(livemode) do
true -> do_process_async(id, user_id, handler, json)
false -> {:error, :ignored_by_environment}
end
end
def process_async(%{"id" => id, "livemode" => livemode} = json, handler) do
case event_matches_environment?(livemode) do
true -> do_process_async(id, nil, handler, json)
false -> {:error, :ignored_by_environment}
end
end

defp do_process_async(id, user_id, handler, json) do
Task.Supervisor.start_child(:webhook_processor, fn -> do_process(id, user_id, handler, json) end)
def process_async(event_params, handler) do
Task.Supervisor.start_child(:webhook_processor, fn -> process(event_params, handler) end)
end

defp do_process(id, user_id, handler, json) do
with {:ok, %Stripe.Event{} = event} <- retrieve_event_from_api(id, user_id),
{:ok, endpoint} <- infer_endpoint_from_handler(handler),
{:ok, %StripeEvent{} = event} <- find_or_create_event(event, endpoint)
do
handle_event(json, event, handler)
else
{:error, :already_processing} -> nil
end
end
@doc """
Used to process a Stripe webhook event.

defp event_matches_environment?(livemode) do
case Application.get_env(:code_corps, :stripe_env) do
:prod -> livemode
_ -> !livemode
end
end
Receives the event JSON as the first parameter.

defp find_or_create_event(%Stripe.Event{} = event, endpoint) do
case find_event(event.id) do
%StripeEvent{status: "processing"} -> {:error, :already_processing}
%StripeEvent{} = event -> {:ok, event}
nil -> create_event(event, endpoint)
end
end
Since a webhook can be a platform or a connect webhook, the function requires
the handler module as the second parameter.

defp find_event(id_from_stripe) do
Repo.get_by(StripeEvent, id_from_stripe: id_from_stripe)
end
# Returns
- `{:ok, %CodeCorps.StripeEvent{}}` if the event was processed in some way. This includes
the event being previously processed, or erroring out, or even just not being handled at the moment.
- `{:error, :already_processing}` if the event already exists locally and is in the process of
being handled.

defp handle_event(json, event, handler) 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
"""
def process(%{"id" => id} = event_params, handler) 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)
end
end

defp infer_endpoint_from_handler(ConnectEventHandler), do: {:ok, "connect"}
defp infer_endpoint_from_handler(PlatformEventHandler), do: {:ok, "platform"}

defp retrieve_event_from_api(id, nil), do: @api.Event.retrieve(id)
defp retrieve_event_from_api(id, user_id), do: @api.Event.retrieve(id, connect_account: user_id)

defp create_event(%Stripe.Event{} = event, endpoint) do
with {:ok, params} <- StripeEventAdapter.to_params(event, %{"endpoint" => endpoint}) do
%StripeEvent{} |> StripeEvent.create_changeset(params) |> Repo.insert
end
end

defp set_errored(%StripeEvent{} = event) do
event |> StripeEvent.update_changeset(%{status: "errored"}) |> Repo.update
end

defp set_processed(%StripeEvent{} = event) do
event |> StripeEvent.update_changeset(%{status: "processed"}) |> Repo.update
end

defp set_unhandled(%StripeEvent{} = event) do
event |> StripeEvent.update_changeset(%{status: "unhandled"}) |> Repo.update
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule CodeCorps.Repo.Migrations.AddIgnoredReasonToStripeEvents do
use Ecto.Migration

def change do
alter table(:stripe_events) do
add :ignored_reason, :string
end
end
end
9 changes: 5 additions & 4 deletions priv/repo/structure.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
-- PostgreSQL database dump
--

-- Dumped from database version 9.5.4
-- Dumped by pg_dump version 9.5.4
-- Dumped from database version 9.5.1
-- Dumped by pg_dump version 9.5.1

SET statement_timeout = 0;
SET lock_timeout = 0;
Expand Down Expand Up @@ -739,7 +739,8 @@ CREATE TABLE stripe_events (
endpoint character varying(255) NOT NULL,
user_id character varying(255),
object_id character varying(255) NOT NULL,
object_type character varying(255) NOT NULL
object_type character varying(255) NOT NULL,
ignored_reason character varying(255)
);


Expand Down Expand Up @@ -2277,5 +2278,5 @@ ALTER TABLE ONLY user_skills
-- PostgreSQL database dump complete
--

INSERT INTO "schema_migrations" (version) VALUES (20160723215749), (20160804000000), (20160804001111), (20160805132301), (20160805203929), (20160808143454), (20160809214736), (20160810124357), (20160815125009), (20160815143002), (20160816020347), (20160816034021), (20160817220118), (20160818000944), (20160818132546), (20160820113856), (20160820164905), (20160822002438), (20160822004056), (20160822011624), (20160822020401), (20160822044612), (20160830081224), (20160830224802), (20160911233738), (20160912002705), (20160912145957), (20160918003206), (20160928232404), (20161003185918), (20161019090945), (20161019110737), (20161020144622), (20161021131026), (20161031001615), (20161121005339), (20161121014050), (20161121043941), (20161121045709), (20161122015942), (20161123081114), (20161123150943), (20161124085742), (20161125200620), (20161126045705), (20161127054559), (20161205024856), (20161207112519), (20161209192504), (20161212005641), (20161214005935), (20161215052051), (20161216051447), (20161218005913), (20161219160401), (20161219163909), (20161220141753), (20161221085759), (20161226213600), (20161231063614), (20170102130055), (20170102181053), (20170104113708), (20170104212623), (20170104235423), (20170106013143), (20170115035159);
INSERT INTO "schema_migrations" (version) VALUES (20160723215749), (20160804000000), (20160804001111), (20160805132301), (20160805203929), (20160808143454), (20160809214736), (20160810124357), (20160815125009), (20160815143002), (20160816020347), (20160816034021), (20160817220118), (20160818000944), (20160818132546), (20160820113856), (20160820164905), (20160822002438), (20160822004056), (20160822011624), (20160822020401), (20160822044612), (20160830081224), (20160830224802), (20160911233738), (20160912002705), (20160912145957), (20160918003206), (20160928232404), (20161003185918), (20161019090945), (20161019110737), (20161020144622), (20161021131026), (20161031001615), (20161121005339), (20161121014050), (20161121043941), (20161121045709), (20161122015942), (20161123081114), (20161123150943), (20161124085742), (20161125200620), (20161126045705), (20161127054559), (20161205024856), (20161207112519), (20161209192504), (20161212005641), (20161214005935), (20161215052051), (20161216051447), (20161218005913), (20161219160401), (20161219163909), (20161220141753), (20161221085759), (20161226213600), (20161231063614), (20170102130055), (20170102181053), (20170104113708), (20170104212623), (20170104235423), (20170106013143), (20170115035159), (20170115230549);

Loading