diff --git a/lib/code_corps/stripe_service/events/account_updated.ex b/lib/code_corps/stripe_service/events/account_updated.ex index e574c422c..b96601d6e 100644 --- a/lib/code_corps/stripe_service/events/account_updated.ex +++ b/lib/code_corps/stripe_service/events/account_updated.ex @@ -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 CodeCorps.StripeService.StripeConnectAccountService.update_from_stripe(id_from_stripe) end end diff --git a/lib/code_corps/stripe_service/events/connect_external_account_created.ex b/lib/code_corps/stripe_service/events/connect_external_account_created.ex index 6f112dfcb..3910127fc 100644 --- a/lib/code_corps/stripe_service/events/connect_external_account_created.ex +++ b/lib/code_corps/stripe_service/events/connect_external_account_created.ex @@ -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 diff --git a/lib/code_corps/stripe_service/events/customer_source_updated.ex b/lib/code_corps/stripe_service/events/customer_source_updated.ex index 829f2bfd7..35f042d1c 100644 --- a/lib/code_corps/stripe_service/events/customer_source_updated.ex +++ b/lib/code_corps/stripe_service/events/customer_source_updated.ex @@ -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 diff --git a/lib/code_corps/stripe_service/events/customer_subscription_deleted.ex b/lib/code_corps/stripe_service/events/customer_subscription_deleted.ex index 8625e9de2..a23d51656 100644 --- a/lib/code_corps/stripe_service/events/customer_subscription_deleted.ex +++ b/lib/code_corps/stripe_service/events/customer_subscription_deleted.ex @@ -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 diff --git a/lib/code_corps/stripe_service/events/customer_subscription_updated.ex b/lib/code_corps/stripe_service/events/customer_subscription_updated.ex index 73fcb8248..043d5108e 100644 --- a/lib/code_corps/stripe_service/events/customer_subscription_updated.ex +++ b/lib/code_corps/stripe_service/events/customer_subscription_updated.ex @@ -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 diff --git a/lib/code_corps/stripe_service/events/customer_updated.ex b/lib/code_corps/stripe_service/events/customer_updated.ex index ac30c2fda..165c8105a 100644 --- a/lib/code_corps/stripe_service/events/customer_updated.ex +++ b/lib/code_corps/stripe_service/events/customer_updated.ex @@ -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 diff --git a/lib/code_corps/stripe_service/events/invoice_payment_succeeded.ex b/lib/code_corps/stripe_service/events/invoice_payment_succeeded.ex index 8b2a42792..9321b5ecc 100644 --- a/lib/code_corps/stripe_service/events/invoice_payment_succeeded.ex +++ b/lib/code_corps/stripe_service/events/invoice_payment_succeeded.ex @@ -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 diff --git a/lib/code_corps/stripe_service/webhook_processing/connect_event_handler.ex b/lib/code_corps/stripe_service/webhook_processing/connect_event_handler.ex index 96c79a180..07f72e04f 100644 --- a/lib/code_corps/stripe_service/webhook_processing/connect_event_handler.ex +++ b/lib/code_corps/stripe_service/webhook_processing/connect_event_handler.ex @@ -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) diff --git a/lib/code_corps/stripe_service/webhook_processing/environment_filter.ex b/lib/code_corps/stripe_service/webhook_processing/environment_filter.ex new file mode 100644 index 000000000..c7c5dbed5 --- /dev/null +++ b/lib/code_corps/stripe_service/webhook_processing/environment_filter.ex @@ -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 diff --git a/lib/code_corps/stripe_service/webhook_processing/event_handler.ex b/lib/code_corps/stripe_service/webhook_processing/event_handler.ex new file mode 100644 index 000000000..c23626c41 --- /dev/null +++ b/lib/code_corps/stripe_service/webhook_processing/event_handler.ex @@ -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 diff --git a/lib/code_corps/stripe_service/webhook_processing/ignored_event_handler.ex b/lib/code_corps/stripe_service/webhook_processing/ignored_event_handler.ex new file mode 100644 index 000000000..2f2b692bb --- /dev/null +++ b/lib/code_corps/stripe_service/webhook_processing/ignored_event_handler.ex @@ -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 diff --git a/lib/code_corps/stripe_service/webhook_processing/platform_event_handler.ex b/lib/code_corps/stripe_service/webhook_processing/platform_event_handler.ex index a1dabc2ec..5bf87f0af 100644 --- a/lib/code_corps/stripe_service/webhook_processing/platform_event_handler.ex +++ b/lib/code_corps/stripe_service/webhook_processing/platform_event_handler.ex @@ -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) diff --git a/lib/code_corps/stripe_service/webhook_processing/webhook_processor.ex b/lib/code_corps/stripe_service/webhook_processing/webhook_processor.ex index 4fa755e99..9981ec374 100644 --- a/lib/code_corps/stripe_service/webhook_processing/webhook_processor.ex +++ b/lib/code_corps/stripe_service/webhook_processing/webhook_processor.ex @@ -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 diff --git a/priv/repo/migrations/20170115230549_add_ignored_reason_to_stripe_events.exs b/priv/repo/migrations/20170115230549_add_ignored_reason_to_stripe_events.exs new file mode 100644 index 000000000..e559cfa46 --- /dev/null +++ b/priv/repo/migrations/20170115230549_add_ignored_reason_to_stripe_events.exs @@ -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 diff --git a/priv/repo/structure.sql b/priv/repo/structure.sql index 15c0f660f..93593965a 100644 --- a/priv/repo/structure.sql +++ b/priv/repo/structure.sql @@ -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; @@ -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) ); @@ -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); diff --git a/test/controllers/stripe_connect_events_controller_test.exs b/test/controllers/stripe_connect_events_controller_test.exs index 60c5f5b14..34dcbd1b2 100644 --- a/test/controllers/stripe_connect_events_controller_test.exs +++ b/test/controllers/stripe_connect_events_controller_test.exs @@ -1,7 +1,7 @@ defmodule CodeCorps.StripeConnectEventsControllerTest do use CodeCorps.ConnCase - alias CodeCorps.{Project, StripeConnectAccount, StripeEvent, StripeExternalAccount, StripeInvoice} + alias CodeCorps.StripeEvent setup do conn = @@ -12,46 +12,6 @@ defmodule CodeCorps.StripeConnectEventsControllerTest do {:ok, conn: conn} end - @account %{ - "id" => "acct_123", - "transfers_enabled" => true - } - - @bank_account %{ - "id" => "ba_19SSZG2eZvKYlo2CXnmzYU5H", - "account" => "acct_1032D82eZvKYlo2C" - } - - @invoice %{ - "charge" => "ch_123", - "customer" => "cus_123", - "id" => "in_123", - "subscription" => "sub_123" - } - - @subscription %{ - "customer" => "cus_123", - "id" => "acct_123", - "status" => "canceled" - } - - defp event_for(object, type) do - %{ - "api_version" => "2016-07-06", - "created" => 1_326_853_478, - "data" => %{ - "object" => object - }, - "id" => "evt_123", - "livemode" => false, - "object" => "event", - "pending_webhooks" => 1, - "request" => nil, - "type" => type, - "user_id" => "acct_123" - } - end - defp wait_for_supervisor, do: wait_for_children(:webhook_processor) # used to have the test wait for or the children of a supervisor to exit @@ -66,260 +26,52 @@ defmodule CodeCorps.StripeConnectEventsControllerTest do assert_receive {:DOWN, ^ref, _, _, _} end - describe "any event" do - test "returns 400, does nothing if event is livemode and env is not :prod", %{conn: conn} do - Application.put_env(:code_corps, :stripe_env, :other) - - event = %{"id" => "evt_123", "livemode" => true, "type" => "any.event"} - - path = conn |> stripe_connect_events_path(:create) - assert conn |> post(path, event) |> response(400) - - wait_for_supervisor - - assert StripeEvent |> Repo.aggregate(:count, :id) == 0 - - # put env back to original state - Application.put_env(:code_corps, :stripe_env, :test) - end - - test "returns 400, does nothing if event is not livemode and env is :prod", %{conn: conn} do - Application.put_env(:code_corps, :stripe_env, :prod) - - event = %{"id" => "evt_123", "livemode" => false, "type" => "any.event"} - - path = conn |> stripe_connect_events_path(:create) - assert conn |> post(path, event) |> response(400) - - wait_for_supervisor - - assert StripeEvent |> Repo.aggregate(:count, :id) == 0 - - # put env back to original state - Application.put_env(:code_corps, :stripe_env, :test) - end - - test "creates event if id is new", %{conn: conn} do - event = %{"id" => "evt_123", "livemode" => false, "type" => "any.event"} - - path = conn |> stripe_connect_events_path(:create) - assert conn |> post(path, event) |> response(200) - - wait_for_supervisor - - event = StripeEvent |> Repo.one - {:ok, mock_api_event} = CodeCorps.StripeTesting.Event.retrieve("evt_123") - - assert event.object_id == mock_api_event.data.object.id - assert event.object_type == "customer" - end - - test "uses existing event if id exists", %{conn: conn} do - insert(:stripe_event, id_from_stripe: "evt_123") - - event = %{"id" => "evt_123", "livemode" => false, "type" => "any.event"} - - path = conn |> stripe_connect_events_path(:create) - assert conn |> post(path, event) |> response(200) - - wait_for_supervisor - - assert StripeEvent |> Repo.aggregate(:count, :id) == 1 - end - - test "sets event as unhandled if event is not handled", %{conn: conn} do - event = %{"id" => "evt_123", "livemode" => false, "type" => "any.event"} - - path = conn |> stripe_connect_events_path(:create) - assert conn |> post(path, event) |> response(200) - - wait_for_supervisor - - record = StripeEvent |> Repo.one - assert record.status == "unhandled" - end - - test "errors out event if handling fails", %{conn: conn} do - # we build the event, but do not make the account, causing it to error out - event = event_for(@account, "account.updated") - - path = conn |> stripe_connect_events_path(:create) - assert conn |> post(path, event) |> response(200) - - wait_for_supervisor - - record = StripeEvent |> Repo.one - assert record.status == "errored" - end - - test "marks event as processed if handling is done", %{conn: conn} do - # we build the event AND create the account, so it should process correctly - event = event_for(@account, "account.updated") - insert(:stripe_connect_account, id_from_stripe: @account["id"]) - - path = conn |> stripe_connect_events_path(:create) - assert conn |> post(path, event) |> response(200) - - wait_for_supervisor - - record = StripeEvent |> Repo.one - assert record.status == "processed" - end - - test "leaves event alone if already processing", %{conn: conn} do - insert(:stripe_event, id_from_stripe: "evt_123", status: "processing") - - event = %{"id" => "evt_123", "livemode" => false, "type" => "any.event"} - - path = conn |> stripe_platform_events_path(:create) - assert conn |> post(path, event) |> response(200) - - wait_for_supervisor - - record = StripeEvent |> Repo.one - assert record.status == "processing" - end - end - - describe "account.external_account.created" do - test "creates an external account record, using stripe params", %{conn: conn} do - event = event_for(@bank_account, "account.external_account.created") - path = stripe_connect_events_path(conn, :create) - - # we expect the event to be associated with an account, so it must be created - insert(:stripe_connect_account, id_from_stripe: @bank_account["account"]) - - assert conn |> post(path, event) |> response(200) - - wait_for_supervisor - - event = Repo.one(StripeEvent) - assert event.status == "processed" - - created_account = Repo.one(StripeExternalAccount) - assert created_account - end - - test "errors out event if no associated connect account", %{conn: conn} do - event = event_for(@bank_account, "account.external_account.created") - path = stripe_connect_events_path(conn, :create) - - assert conn |> post(path, event) |> response(200) - - wait_for_supervisor - - event = Repo.one(StripeEvent) - assert event.status == "errored" - - assert [] == Repo.all(StripeExternalAccount) - end - end - - describe "account.updated" do - test "updates account when one matches", %{conn: conn} do - event = event_for(@account, "account.updated") - stripe_id = @account["id"] + test "responds with 200 when the event will be processed", %{conn: conn} do + event = %{"id" => "evt_123", "livemode" => false, "type" => "any.event"} - insert(:stripe_connect_account, - id_from_stripe: stripe_id, - transfers_enabled: false - ) + path = conn |> stripe_connect_events_path(:create) + assert conn |> post(path, event) |> response(200) - path = stripe_connect_events_path(conn, :create) - assert conn |> post(path, event) |> response(200) + wait_for_supervisor - wait_for_supervisor - - updated_account = Repo.get_by(StripeConnectAccount, id_from_stripe: stripe_id) - assert updated_account.transfers_enabled - end + assert StripeEvent |> Repo.aggregate(:count, :id) == 1 end - describe "customer.subscription.updated" do - test "updates subscription when one matches", %{conn: conn} do - event = event_for(@subscription, "customer.subscription.updated") - stripe_id = @subscription["id"] - connect_customer_id = @subscription["customer"] - - project = insert(:project, total_monthly_donated: 1000) - account = insert(:stripe_connect_account) - platform_customer = insert(:stripe_platform_customer) + # TODO: The following two can be merged into one and actual environment matching behavior + # can be added to the EnvironmentFilter test module + # + # TODO: Can also probably move the supervisor stuff to the webhook processor module test + # (the group of tests which will eventually test async behavior) - insert(:stripe_connect_customer, - id_from_stripe: connect_customer_id, - stripe_connect_account: account, - stripe_platform_customer: platform_customer) + test "returns 400, does nothing if event is livemode and env is not :prod", %{conn: conn} do + Application.put_env(:code_corps, :stripe_env, :other) - plan = insert(:stripe_connect_plan, project: project) + event = %{"id" => "evt_123", "livemode" => true, "type" => "any.event"} - insert(:stripe_connect_subscription, - id_from_stripe: stripe_id, - stripe_connect_plan: plan) + path = conn |> stripe_connect_events_path(:create) + assert conn |> post(path, event) |> response(400) - path = stripe_connect_events_path(conn, :create) - assert conn |> post(path, event) |> response(200) + wait_for_supervisor - wait_for_supervisor + assert StripeEvent |> Repo.aggregate(:count, :id) == 0 - updated_project = Repo.get_by(Project, id: project.id) - assert updated_project.total_monthly_donated == 0 - end + # put env back to original state + Application.put_env(:code_corps, :stripe_env, :test) end - describe "customer.subscription.deleted" do - test "sets subscription to inactive when one matches", %{conn: conn} do - event = event_for(@subscription, "customer.subscription.deleted") - stripe_id = @subscription["id"] - connect_customer_id = @subscription["customer"] - - project = insert(:project, total_monthly_donated: 1000) - account = insert(:stripe_connect_account) - platform_customer = insert(:stripe_platform_customer) - - insert(:stripe_connect_customer, - id_from_stripe: connect_customer_id, - stripe_connect_account: account, - stripe_platform_customer: platform_customer) - - plan = insert(:stripe_connect_plan, project: project) - - insert(:stripe_connect_subscription, - id_from_stripe: stripe_id, - stripe_connect_plan: plan) - - path = stripe_connect_events_path(conn, :create) - assert conn |> post(path, event) |> response(200) - - wait_for_supervisor - - updated_project = Repo.get_by(Project, id: project.id) - assert updated_project.total_monthly_donated == 0 - end - end + test "returns 400, does nothing if event is not livemode and env is :prod", %{conn: conn} do + Application.put_env(:code_corps, :stripe_env, :prod) - describe "invoice.payment_succeeded" do - test "sets subscription to inactive when one matches", %{conn: conn} do - event = event_for(@invoice, "invoice.payment_succeeded") - stripe_id = @invoice["id"] - connect_customer_id = @invoice["customer"] - connect_subscription_id = @invoice["subscription"] + event = %{"id" => "evt_123", "livemode" => false, "type" => "any.event"} - user = insert(:user) - insert(:stripe_connect_subscription, id_from_stripe: connect_subscription_id) - stripe_platform_customer = insert(:stripe_platform_customer, user: user) - insert(:stripe_connect_customer, - id_from_stripe: connect_customer_id, - stripe_platform_customer: stripe_platform_customer, - user: user - ) + path = conn |> stripe_connect_events_path(:create) + assert conn |> post(path, event) |> response(400) - path = stripe_connect_events_path(conn, :create) - assert conn |> post(path, event) |> response(200) + wait_for_supervisor - wait_for_supervisor + assert StripeEvent |> Repo.aggregate(:count, :id) == 0 - invoice = Repo.one(StripeInvoice) - assert invoice.id_from_stripe == stripe_id - end + # put env back to original state + Application.put_env(:code_corps, :stripe_env, :test) end end diff --git a/test/controllers/stripe_platform_events_controller_test.exs b/test/controllers/stripe_platform_events_controller_test.exs index 4e7412287..087ca1062 100644 --- a/test/controllers/stripe_platform_events_controller_test.exs +++ b/test/controllers/stripe_platform_events_controller_test.exs @@ -1,7 +1,7 @@ defmodule CodeCorps.StripePlatformEventsControllerTest do use CodeCorps.ConnCase - alias CodeCorps.{StripeEvent, StripePlatformCard, StripePlatformCustomer} + alias CodeCorps.StripeEvent setup do conn = @@ -12,28 +12,6 @@ defmodule CodeCorps.StripePlatformEventsControllerTest do {:ok, conn: conn} end - @card %{ - "id" => "card_19LEnDBKl1F6IRFfjLfJRYuN", - "object" => "card", - "customer" => "cus_9e9KNE2beHhfLy" - } - - defp event_for(object, type) do - %{ - "api_version" => "2016-07-06", - "created" => 1326853478, - "data" => %{ - "object" => object - }, - "id" => "evt_00000000000000", - "livemode" => false, - "object" => "event", - "pending_webhooks" => 1, - "request" => nil, - "type" => type - } - end - defp wait_for_supervisor, do: wait_for_children(:webhook_processor) # used to have the test wait for or the children of a supervisor to exit @@ -49,157 +27,52 @@ defmodule CodeCorps.StripePlatformEventsControllerTest do assert_receive {:DOWN, ^ref, _, _, _} end - describe "customer.updated" do - @customer %{ - "id" => "cus_1234567" - } - - test "returns 200 and updates platform customer", %{conn: conn} do - stripe_id = @customer["id"] - platform_customer = insert(:stripe_platform_customer, id_from_stripe: stripe_id) - - event = event_for(@customer, "customer.updated") - - path = conn |> stripe_platform_events_path(:create) - assert conn |> post(path, event) |> response(200) + test "responds with 200 when the event will be processed", %{conn: conn} do + event = %{"id" => "evt_123", "livemode" => false, "type" => "any.event"} - wait_for_supervisor + path = conn |> stripe_platform_events_path(:create) + assert conn |> post(path, event) |> response(200) - platform_customer = Repo.get(StripePlatformCustomer, platform_customer.id) + wait_for_supervisor - # hardcoded in StripeTesting.Customer - assert platform_customer.email == "hardcoded@test.com" - end + assert StripeEvent |> Repo.aggregate(:count, :id) == 1 end - describe "customer.source.updated" do - test "returns 200 and updates card when one matches", %{conn: conn} do - event = event_for(@card, "customer.source.updated") - stripe_id = @card["id"] - platform_customer_id = @card["customer"] - - insert(:stripe_platform_customer, id_from_stripe: platform_customer_id) - platform_card = insert(:stripe_platform_card, id_from_stripe: stripe_id, customer_id_from_stripe: platform_customer_id) - - path = stripe_platform_events_path(conn, :create) - assert conn |> post(path, event) |> response(200) - - wait_for_supervisor - - updated_card = Repo.get_by(StripePlatformCard, id: platform_card.id) - # hardcoded in StripeTesting.Card - assert updated_card.name == "John Doe" - end - end - - describe "any event" do - test "returns 400, does nothing if event is livemode and env is not :prod", %{conn: conn} do - Application.put_env(:code_corps, :stripe_env, :other) - - event = %{"id" => "evt_123", "livemode" => true, "type" => "any.event"} - - path = conn |> stripe_platform_events_path(:create) - assert conn |> post(path, event) |> response(400) - - wait_for_supervisor - - assert StripeEvent |> Repo.aggregate(:count, :id) == 0 - - # put env back to original state - Application.put_env(:code_corps, :stripe_env, :test) - end - - test "returns 400, does nothing if event is not livemode and env is :prod", %{conn: conn} do - Application.put_env(:code_corps, :stripe_env, :prod) - - event = %{"id" => "evt_123", "livemode" => false, "type" => "any.event"} - - path = conn |> stripe_platform_events_path(:create) - assert conn |> post(path, event) |> response(400) + # TODO: The following two can be merged into one and actual environment matching behavior + # can be added to the EnvironmentFilter test module + # + # TODO: Can also probably move the supervisor stuff to the webhook processor module test + # (the group of tests which will eventually test async behavior) - wait_for_supervisor + test "returns 400, does nothing if event is livemode and env is not :prod", %{conn: conn} do + Application.put_env(:code_corps, :stripe_env, :other) - assert StripeEvent |> Repo.aggregate(:count, :id) == 0 + event = %{"id" => "evt_123", "livemode" => true, "type" => "any.event"} - # put env back to original state - Application.put_env(:code_corps, :stripe_env, :test) - end + path = conn |> stripe_platform_events_path(:create) + assert conn |> post(path, event) |> response(400) - test "creates event if id is new", %{conn: conn} do - event = %{"id" => "evt_123", "livemode" => false, "type" => "any.event"} + wait_for_supervisor - path = conn |> stripe_platform_events_path(:create) - assert conn |> post(path, event) |> response(200) + assert StripeEvent |> Repo.aggregate(:count, :id) == 0 - wait_for_supervisor - - assert StripeEvent |> Repo.aggregate(:count, :id) == 1 - end - - test "uses existing event if id exists", %{conn: conn} do - insert(:stripe_event, id_from_stripe: "evt_123") - - event = %{"id" => "evt_123", "livemode" => false, "type" => "any.event"} - - path = conn |> stripe_platform_events_path(:create) - assert conn |> post(path, event) |> response(200) - - wait_for_supervisor - - assert StripeEvent |> Repo.aggregate(:count, :id) == 1 - end - - test "sets event as unhandled if event is not handled", %{conn: conn} do - event = %{"id" => "evt_123", "livemode" => false, "type" => "any.event"} - - path = conn |> stripe_platform_events_path(:create) - assert conn |> post(path, event) |> response(200) - - wait_for_supervisor - - record = StripeEvent |> Repo.one - assert record.status == "unhandled" - end - - test "errors out event if handling fails", %{conn: conn} do - # we build the event, but do not make the customer, causing it to error out - event = event_for(@customer, "customer.updated") - - path = conn |> stripe_platform_events_path(:create) - assert conn |> post(path, event) |> response(200) - - wait_for_supervisor - - record = StripeEvent |> Repo.one - assert record.status == "errored" - end - - test "marks event as processed if handling is done", %{conn: conn} do - # we build the event AND create the customer, so it should process correctly - event = event_for(@customer, "customer.updated") - insert(:stripe_platform_customer, id_from_stripe: @customer["id"]) - - path = conn |> stripe_platform_events_path(:create) - assert conn |> post(path, event) |> response(200) - - wait_for_supervisor + # put env back to original state + Application.put_env(:code_corps, :stripe_env, :test) + end - record = StripeEvent |> Repo.one - assert record.status == "processed" - end + test "returns 400, does nothing if event is not livemode and env is :prod", %{conn: conn} do + Application.put_env(:code_corps, :stripe_env, :prod) - test "leaves event alone if already processing", %{conn: conn} do - insert(:stripe_event, id_from_stripe: "evt_123", status: "processing") + event = %{"id" => "evt_123", "livemode" => false, "type" => "any.event"} - event = %{"id" => "evt_123", "livemode" => false, "type" => "any.event"} + path = conn |> stripe_platform_events_path(:create) + assert conn |> post(path, event) |> response(400) - path = conn |> stripe_platform_events_path(:create) - assert conn |> post(path, event) |> response(200) + wait_for_supervisor - wait_for_supervisor + assert StripeEvent |> Repo.aggregate(:count, :id) == 0 - record = StripeEvent |> Repo.one - assert record.status == "processing" - end + # put env back to original state + Application.put_env(:code_corps, :stripe_env, :test) end end diff --git a/test/lib/code_corps/stripe_service/webhook_processing/event_handler_test.exs b/test/lib/code_corps/stripe_service/webhook_processing/event_handler_test.exs new file mode 100644 index 000000000..88ccac7bb --- /dev/null +++ b/test/lib/code_corps/stripe_service/webhook_processing/event_handler_test.exs @@ -0,0 +1,247 @@ +defmodule CodeCorps.StripeService.WebhookProcessing.EventHandlerTest do + use CodeCorps.ModelCase + + alias CodeCorps.StripeService.WebhookProcessing.{ + ConnectEventHandler, EventHandler, PlatformEventHandler + } + + alias CodeCorps.{ + StripeEvent, StripeExternalAccount, StripeInvoice, StripePlatformCard, StripePlatformCustomer + } + + defmodule CodeCorps.StripeService.WebhookProcessing.EventHandlerTest.StubObject do + defstruct [:id] + end + + defp stub_object do + %CodeCorps.StripeService.WebhookProcessing.EventHandlerTest.StubObject{id: "stub_id"} + end + + defp build_event, do: build_event("any.event") + defp build_event(type), do: build_event(type, stub_object) + defp build_event(type, object), do: build_event("some_id", type, object) + defp build_event(id, type, object), do: %Stripe.Event{id: id, type: type, data: %{object: object}} + + describe "platform events" do + test "handles customer.updated" do + platform_customer = insert(:stripe_platform_customer) + stripe_customer = %Stripe.Customer{id: platform_customer.id_from_stripe} + event = build_event("customer.updated", stripe_customer) + + {:ok, event} = EventHandler.handle(event, PlatformEventHandler) + assert event.object_type == "customer" + assert event.object_id == platform_customer.id_from_stripe + assert event.status == "processed" + + platform_customer = Repo.get(StripePlatformCustomer, platform_customer.id) + + # hardcoded in StripeTesting.Customer + assert platform_customer.email == "hardcoded@test.com" + end + + test "handles customer.source.updated" do + platform_customer = insert(:stripe_platform_customer) + platform_card = insert(:stripe_platform_card, customer_id_from_stripe: platform_customer.id_from_stripe) + stripe_card = %Stripe.Card{id: platform_card.id_from_stripe} + event = build_event("customer.source.updated", stripe_card) + + {:ok, event} = EventHandler.handle(event, PlatformEventHandler) + assert event.object_type == "card" + assert event.object_id == platform_card.id_from_stripe + assert event.status == "processed" + + updated_card = Repo.get_by(StripePlatformCard, id: platform_card.id) + + # hardcoded in StripeTesting.Card + assert updated_card.name == "John Doe" + end + end + + describe "connect events" do + test "handles account.external_account.created" do + connect_account = insert(:stripe_connect_account) + event = build_event( + "account.external_account.created", + %Stripe.ExternalAccount{id: "ext_123", account: connect_account.id_from_stripe} + ) + + {:ok, event} = EventHandler.handle(event, ConnectEventHandler) + assert event.object_type == "external_account" + assert event.object_id == "ext_123" + assert event.status == "processed" + + assert Repo.get_by(StripeExternalAccount, id_from_stripe: "ext_123") + end + + test "handles account.updated" do + connect_account = insert(:stripe_connect_account) + event = build_event("account.updated", %Stripe.Account{id: connect_account.id_from_stripe}) + + {:ok, event} = EventHandler.handle(event, ConnectEventHandler) + assert event.object_type == "account" + assert event.object_id == connect_account.id_from_stripe + assert event.status == "processed" + end + + test "handles customer.subscription.updated" do + project = insert(:project) + plan = insert(:stripe_connect_plan, project: project) + subscription = insert(:stripe_connect_subscription, stripe_connect_plan: plan) + + account = insert(:stripe_connect_account) + platform_customer = insert(:stripe_platform_customer) + connect_customer = insert( + :stripe_connect_customer, + stripe_connect_account: account, + stripe_platform_customer: platform_customer + ) + + event = build_event( + "customer.subscription.updated", + %Stripe.Subscription{ + id: subscription.id_from_stripe, + customer: connect_customer.id_from_stripe + } + ) + + {:ok, event} = EventHandler.handle(event, ConnectEventHandler) + assert event.object_type == "subscription" + assert event.object_id == subscription.id_from_stripe + assert event.status == "processed" + end + + test "handles customer.subscription.deleted" do + project = insert(:project) + plan = insert(:stripe_connect_plan, project: project) + subscription = insert(:stripe_connect_subscription, stripe_connect_plan: plan) + + account = insert(:stripe_connect_account) + platform_customer = insert(:stripe_platform_customer) + connect_customer = insert( + :stripe_connect_customer, + stripe_connect_account: account, + stripe_platform_customer: platform_customer + ) + + event = build_event( + "customer.subscription.deleted", + %Stripe.Subscription{ + id: subscription.id_from_stripe, + customer: connect_customer.id_from_stripe + } + ) + + {:ok, event} = EventHandler.handle(event, ConnectEventHandler) + assert event.object_type == "subscription" + assert event.object_id == subscription.id_from_stripe + assert event.status == "processed" + end + + test "handles invoice.payment_succeeded" do + user = insert(:user) + # need to hardcode id from stripe, since this is the value StripeTesting returns + subscription = insert(:stripe_connect_subscription, id_from_stripe: "sub_123") + stripe_platform_customer = insert(:stripe_platform_customer, user: user) + # same with hardcoding customer id from stripe + connect_customer = insert( + :stripe_connect_customer, + id_from_stripe: "cus_123", + stripe_platform_customer: stripe_platform_customer, + user: user + ) + + event = build_event( + "invoice.payment_succeeded", + %Stripe.Invoice{ + id: "ivc_123", + customer: connect_customer.id_from_stripe, + subscription: subscription.id_from_stripe + } + ) + + {:ok, event} = EventHandler.handle(event, ConnectEventHandler) + assert event.object_type == "invoice" + assert event.object_id == "ivc_123" + assert event.status == "processed" + + assert Repo.get_by(StripeInvoice, id_from_stripe: "ivc_123") + end + end + + describe "any event" do + test "sets endpoint to 'platform' when using PlatformEventHandler" do + event = build_event + + {:ok, event} = EventHandler.handle(event, PlatformEventHandler) + assert event.endpoint == "platform" + end + + test "sets endpoint to 'connect' when using ConnectEventHandler" do + event = build_event + + {:ok, event} = EventHandler.handle(event, ConnectEventHandler) + assert event.endpoint == "connect" + end + + test "creates event if id is new" do + event = build_event + {:ok, event} = EventHandler.handle(event, PlatformEventHandler) + + assert event.id_from_stripe == "some_id" + assert event.object_id == "stub_id" + assert event.object_type == "stub_object" + assert event.status == "unhandled" + end + + test "uses existing event if id exists" do + local_event = insert(:stripe_event) + event = build_event(local_event.id_from_stripe, "any.event", stub_object) + + {:ok, returned_event} = EventHandler.handle(event, PlatformEventHandler) + assert returned_event.id == local_event.id + + assert StripeEvent |> Repo.aggregate(:count, :id) == 1 + end + + test "sets event as unhandled if event is not handled" do + event = build_event("unhandled.event") + + {:ok, event} = EventHandler.handle(event, PlatformEventHandler) + assert event.status == "unhandled" + end + + test "errors out event if handling fails" do + # we build the event, but do not make the customer, causing it to error out + event = build_event("customer.updated", %Stripe.Customer{id: "some_id"}) + + {:ok, event} = EventHandler.handle(event, PlatformEventHandler) + assert event.status == "errored" + end + + test "marks event as processed if handling is done" do + # we build the event AND create the customer, so it should process correctly + event = build_event("customer.updated", %Stripe.Customer{id: "some_id"}) + insert(:stripe_platform_customer, id_from_stripe: "some_id") + + {:ok, event} = EventHandler.handle(event, PlatformEventHandler) + assert event.status == "processed" + end + + test "leaves event alone if already processing" do + local_event = insert(:stripe_event, status: "processing") + event = build_event(local_event.id_from_stripe, "any.event", %Stripe.Customer{id: "some_id"}) + + assert {:error, :already_processing} == EventHandler.handle(event, PlatformEventHandler) + end + end + + describe "ignored events" do + test "properly sets as ignored" do + event = build_event("application_fee.created") + {:ok, event} = EventHandler.handle(event, PlatformEventHandler) + + assert event.status == "ignored" + assert event.ignored_reason + end + end +end diff --git a/test/lib/code_corps/stripe_service/webhook_processing/ignored_event_handler_test.exs b/test/lib/code_corps/stripe_service/webhook_processing/ignored_event_handler_test.exs new file mode 100644 index 000000000..a7648c08a --- /dev/null +++ b/test/lib/code_corps/stripe_service/webhook_processing/ignored_event_handler_test.exs @@ -0,0 +1,30 @@ +defmodule CodeCorps.StripeService.WebhookProcessing.IgnoredEventHandlerTest do + use CodeCorps.ModelCase + + alias CodeCorps.StripeService.WebhookProcessing.IgnoredEventHandler + + defp ignored?(type) do + event = insert(:stripe_event, type: type) + {:ok, event} = IgnoredEventHandler.handle(event) + + event.ignored_reason && event.status == "ignored" + end + + describe "handle/1" do + test "ignores events from the ignored events list" do + IgnoredEventHandler.ignored_event_types + |> Enum.each(fn(type) -> assert ignored?(type) end) + + assert_raise(FunctionClauseError, fn -> ignored?("some.other.type") end) + end + end + + describe "should_handle?/1" do + test "returns true for types from the ignored list" do + IgnoredEventHandler.ignored_event_types + |> Enum.each(fn(type) -> assert IgnoredEventHandler.should_handle?(type) end) + + refute IgnoredEventHandler.should_handle?("some.other.type") + end + end + end diff --git a/web/controllers/stripe_connect_events_controller.ex b/web/controllers/stripe_connect_events_controller.ex index 6eb5570d8..2dd178576 100644 --- a/web/controllers/stripe_connect_events_controller.ex +++ b/web/controllers/stripe_connect_events_controller.ex @@ -1,12 +1,17 @@ defmodule CodeCorps.StripeConnectEventsController do use CodeCorps.Web, :controller - alias CodeCorps.StripeService.WebhookProcessing.{ConnectEventHandler, WebhookProcessor} + alias CodeCorps.StripeService.WebhookProcessing.{ + ConnectEventHandler, EnvironmentFilter, WebhookProcessor + } - def create(conn, params) do - case WebhookProcessor.process_async(params, ConnectEventHandler) do - {:ok, _pid} -> conn |> send_resp(200, "") - {:error, :ignored_by_environment} -> conn |> send_resp(400, "") + def create(conn, event_params) do + case EnvironmentFilter.environment_matches?(event_params) do + true -> + {:ok, _pid} = WebhookProcessor.process_async(event_params, ConnectEventHandler) + conn |> send_resp(200, "") + false -> + conn |> send_resp(400, "") end end end diff --git a/web/controllers/stripe_platform_events_controller.ex b/web/controllers/stripe_platform_events_controller.ex index 09e971cae..85c32d99e 100644 --- a/web/controllers/stripe_platform_events_controller.ex +++ b/web/controllers/stripe_platform_events_controller.ex @@ -1,12 +1,17 @@ defmodule CodeCorps.StripePlatformEventsController do use CodeCorps.Web, :controller - alias CodeCorps.StripeService.WebhookProcessing.{PlatformEventHandler, WebhookProcessor} + alias CodeCorps.StripeService.WebhookProcessing.{ + EnvironmentFilter, PlatformEventHandler, WebhookProcessor + } - def create(conn, params) do - case WebhookProcessor.process_async(params, PlatformEventHandler) do - {:ok, _pid} -> conn |> send_resp(200, "") - {:error, :ignored_by_environment} -> conn |> send_resp(400, "") + def create(conn, event_params) do + case EnvironmentFilter.environment_matches?(event_params) do + true -> + {:ok, _pid} = WebhookProcessor.process_async(event_params, PlatformEventHandler) + conn |> send_resp(200, "") + false -> + conn |> send_resp(400, "") end end end diff --git a/web/models/stripe_event.ex b/web/models/stripe_event.ex index f397550e0..7357c5519 100644 --- a/web/models/stripe_event.ex +++ b/web/models/stripe_event.ex @@ -24,6 +24,7 @@ defmodule CodeCorps.StripeEvent do schema "stripe_events" do field :endpoint, :string, null: false field :id_from_stripe, :string, null: false + field :ignored_reason, :string field :object_id, :string field :object_type, :string field :status, :string, default: "unprocessed" @@ -54,7 +55,7 @@ defmodule CodeCorps.StripeEvent do """ def update_changeset(struct, params) do struct - |> cast(params, [:status]) + |> cast(params, [:ignored_reason, :status]) |> validate_required([:status]) |> validate_inclusion(:status, states) end @@ -64,6 +65,6 @@ defmodule CodeCorps.StripeEvent do end defp states do - ~w{ errored processed processing unhandled unprocessed } + ~w{ errored ignored processed processing unhandled unprocessed } end end