berrypod/lib/berrypod_web/controllers/stripe_webhook_controller.ex

179 lines
5.5 KiB
Elixir
Raw Normal View History

defmodule BerrypodWeb.StripeWebhookController do
use BerrypodWeb, :controller
alias Berrypod.Orders
alias Berrypod.Orders.{OrderNotifier, OrderSubmissionWorker}
require Logger
def handle(conn, _params) do
raw_body = conn.assigns[:raw_body] || ""
signature = List.first(get_req_header(conn, "stripe-signature")) || ""
signing_secret = Application.get_env(:stripity_stripe, :signing_secret) || ""
case Stripe.Webhook.construct_event(raw_body, signature, signing_secret) do
{:ok, %Stripe.Event{} = event} ->
handle_event(event)
json(conn, %{received: true})
{:error, reason} ->
Logger.warning("Stripe webhook verification failed: #{inspect(reason)}")
conn
|> put_status(401)
|> json(%{error: "Invalid signature"})
end
end
defp handle_event(%Stripe.Event{type: "checkout.session.completed", data: %{object: session}}) do
order_id = session.metadata["order_id"]
case Orders.get_order(order_id) do
nil ->
Logger.warning("Stripe webhook: order not found for id=#{order_id}")
order ->
payment_intent_id = session.payment_intent
{:ok, order} = Orders.mark_paid(order, payment_intent_id)
# Update shipping cost from Stripe (if shipping options were presented)
order = update_shipping_cost(order, session)
# Update shipping address if collected by Stripe
order =
if session.shipping_details do
{:ok, updated} = update_shipping(order, session.shipping_details)
updated
else
order
end
# Update customer email from Stripe session
order =
if session.customer_details && session.customer_details.email do
{:ok, updated} =
Orders.update_order(order, %{customer_email: session.customer_details.email})
updated
else
order
end
# Reload items for the email (update_order doesn't preload)
order = Orders.get_order(order.id)
# Broadcast to success page via PubSub
Phoenix.PubSub.broadcast(
Berrypod.PubSub,
"order:#{order.id}:status",
{:order_paid, order}
)
OrderNotifier.deliver_order_confirmation(order)
# Submit to fulfilment provider
if order.shipping_address && order.shipping_address != %{} do
OrderSubmissionWorker.enqueue(order.id)
else
Logger.warning(
"Order #{order.order_number} paid but no shipping address — manual submit needed"
)
end
Logger.info("Order #{order.order_number} marked as paid")
end
end
defp handle_event(%Stripe.Event{type: "checkout.session.expired", data: %{object: session}}) do
order_id = session.metadata["order_id"]
order = Orders.get_order(order_id)
if order, do: Orders.mark_failed(order)
Logger.info("Stripe checkout session expired for order #{order_id}")
if Berrypod.Settings.abandoned_cart_recovery_enabled?() do
maybe_create_abandoned_cart(session, order)
end
end
defp handle_event(%Stripe.Event{type: type}) do
Logger.debug("Unhandled Stripe event: #{type}")
end
defp maybe_create_abandoned_cart(_session, nil), do: :ok
defp maybe_create_abandoned_cart(session, order) do
email = session.customer_details && session.customer_details.email
cond do
is_nil(email) or email == "" ->
Logger.debug("Cart recovery: no email captured for session #{session.id}")
Orders.check_suppression(email) == :suppressed ->
Logger.debug("Cart recovery: #{email} is suppressed")
Orders.has_recent_paid_order?(email) ->
Logger.debug("Cart recovery: #{email} has a recent paid order, skipping")
not is_nil(Orders.get_abandoned_cart_by_session(session.id)) ->
Logger.debug("Cart recovery: session #{session.id} already recorded")
true ->
create_cart_and_enqueue(session, order, email)
end
end
defp create_cart_and_enqueue(session, order, email) do
expired_at = DateTime.utc_now() |> DateTime.truncate(:second)
attrs = %{
customer_email: email,
order_id: order.id,
stripe_session_id: session.id,
cart_total: order.total,
expired_at: expired_at
}
case Orders.create_abandoned_cart(attrs) do
{:ok, cart} ->
Berrypod.Orders.AbandonedCartEmailWorker.enqueue(cart.id)
Logger.info("Abandoned cart recorded for #{email}, recovery email scheduled")
{:error, reason} ->
Logger.warning("Failed to create abandoned cart: #{inspect(reason)}")
end
end
defp update_shipping(order, shipping_details) do
address = shipping_details.address || %{}
shipping_address = %{
"name" => shipping_details.name,
"line1" => address.line1,
"line2" => address.line2,
"city" => address.city,
"postal_code" => address.postal_code,
"state" => address.state,
"country" => address.country
}
Orders.update_order(order, %{shipping_address: shipping_address})
end
defp update_shipping_cost(order, session) do
shipping_amount = session.shipping_cost && session.shipping_cost.amount_total
if is_integer(shipping_amount) and shipping_amount > 0 do
new_total = order.subtotal + shipping_amount
case Orders.update_order(order, %{shipping_cost: shipping_amount, total: new_total}) do
{:ok, updated} -> updated
{:error, _} -> order
end
else
order
end
end
end