defmodule GlificWeb.StripeController do @moduledoc """ The controller for all events received from Stripe """ use GlificWeb, :controller alias Glific.Partners.{ Billing, Invoice } alias Glific.Repo require Logger @doc """ The top level API used by the router. Use pattern matching to handle specific events """ @spec stripe_webhook(Plug.Conn.t(), map()) :: Plug.Conn.t() def stripe_webhook( %Plug.Conn{assigns: %{stripe_event: stripe_event, organization_id: organization_id}} = conn, _params ) do organization_id = organization_id(stripe_event, organization_id) case handle_webhook(stripe_event, organization_id) do {:ok, _} -> handle_success(conn) {:error, error} -> handle_error(conn, error) end end ## We might need to move this to stripe webhook plug. ## I am just not sure that how it will impact on other request and if the ## customer id is present in all endpoints. @spec organization_id(any(), non_neg_integer) :: non_neg_integer defp organization_id(stripe_event, default) do customer_id = get_customer_id(stripe_event) with true <- is_struct(stripe_event.data.object), {:ok, billing} <- Repo.fetch_by(Billing, %{stripe_customer_id: customer_id}, skip_organization_id: true) do Logger.info("Stripe webhook: #{stripe_event.type}, org: #{billing.organization_id}") Repo.put_process_state(billing.organization_id) billing.organization_id else _ -> default end end @spec get_customer_id(map()) :: String.t() defp get_customer_id(%{type: type, data: %{object: object}}) when type in ["customer.deleted", "customer.updated"] do object.id end defp get_customer_id(%{data: %{object: object}}), do: object.customer @spec handle_success(Plug.Conn.t()) :: Plug.Conn.t() defp handle_success(conn) do conn |> put_resp_content_type("text/plain") |> send_resp(200, "ok") end @spec handle_error(Plug.Conn.t(), String.t()) :: Plug.Conn.t() defp handle_error(conn, error) do conn |> put_resp_content_type("text/plain") |> send_resp(422, error) end @spec handle_webhook(map(), non_neg_integer) :: {:ok | :error, String.t()} defp handle_webhook( %{type: "invoice.created", data: %{object: invoice}} = _stripe_event, organization_id ) do Billing.credit_customer(%{ invoice_id: invoice.id, organization_id: organization_id, status: invoice.status, amount_due: invoice.subtotal }) case Invoice.create_invoice(%{stripe_invoice: invoice, organization_id: organization_id}) do {:ok, invoice} -> {:ok, "success, #{invoice.id}"} {:error, error} -> {:error, error} end end defp handle_webhook( %{type: "invoice.payment_succeeded", data: %{object: invoice}} = _stripe_event, _organization_id ), do: Invoice.update_invoice_status(invoice.id, "paid") defp handle_webhook( %{type: "invoice.payment_failed", data: %{object: invoice}} = _stripe_event, _organization_id ), do: Invoice.update_invoice_status(invoice.id, "payment_failed") defp handle_webhook( %{type: "customer.subscription.updated", data: %{object: subscription}} = _stripe_event, organization_id ), do: Billing.update_subscription_details(subscription, organization_id, nil) defp handle_webhook( %{type: "customer.subscription.created", data: %{object: subscription}} = _stripe_event, organization_id ), do: Billing.subscription_created_callback(subscription, organization_id) defp handle_webhook( %{type: "customer.updated", data: %{object: customer}} = _stripe_event, organization_id ) do Billing.get_billing(%{stripe_customer_id: customer.id, organization_id: organization_id}) |> then( &if is_nil(&1), do: {:ok, "No Billing corresponding to customer id #{customer.id} exist"}, else: Billing.update_billing(&1, %{email: customer.email}) ) end defp handle_webhook( %{type: "customer.deleted"} = stripe_event, organization_id ) do customer_id = get_customer_id(stripe_event) Billing.get_billing(%{stripe_customer_id: customer_id, organization_id: organization_id}) |> Billing.delete_billing() end defp handle_webhook(stripe_event, _organization_id) do # handle default case. We ignore these web hooks. {:ok, "success, ignoring #{stripe_event.type}"} end end