berrypod/lib/berrypod_web/controllers/checkout_controller.ex
jamey 67a26eb6b4
All checks were successful
deploy / deploy (push) Successful in 1m26s
add contextual prompts for skipped setup steps
Disable checkout when Stripe isn't connected (cart drawer, cart page,
and early guard in checkout controller to prevent orphaned orders).
Show amber warning on order detail when email isn't configured.
Fix pre-existing missing vertical spacing between page blocks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 14:02:49 +00:00

166 lines
4.7 KiB
Elixir

defmodule BerrypodWeb.CheckoutController do
use BerrypodWeb, :controller
alias Berrypod.{Analytics, Cart, Settings}
alias Berrypod.Orders
alias Berrypod.Shipping
require Logger
def create(conn, _params) do
unless Settings.has_secret?("stripe_api_key") do
conn
|> put_flash(:error, "Checkout isn't available yet")
|> redirect(to: ~p"/cart")
else
cart_items = Cart.get_from_session(get_session(conn))
hydrated = Cart.hydrate(cart_items)
if hydrated == [] do
conn
|> put_flash(:error, "Your basket is empty")
|> redirect(to: ~p"/cart")
else
track_checkout_start(conn)
create_checkout(conn, hydrated)
end
end
end
defp create_checkout(conn, hydrated_items) do
# Create a pending order with price snapshots
case Orders.create_order(%{items: hydrated_items}) do
{:ok, order} ->
create_stripe_session(conn, order, hydrated_items)
{:error, _changeset} ->
Logger.error("Failed to create order")
conn
|> put_flash(:error, "Something went wrong. Please try again.")
|> redirect(to: ~p"/cart")
end
end
defp create_stripe_session(conn, order, hydrated_items) do
line_items =
Enum.map(hydrated_items, fn item ->
product_name =
if item.variant,
do: "#{item.name}#{item.variant}",
else: item.name
%{
price_data: %{
currency: "gbp",
unit_amount: item.price,
product_data: %{name: product_name}
},
quantity: item.quantity
}
end)
base_url = BerrypodWeb.Endpoint.url()
params =
%{
mode: "payment",
line_items: line_items,
success_url: "#{base_url}/checkout/success?session_id={CHECKOUT_SESSION_ID}",
cancel_url: "#{base_url}/cart",
metadata: %{"order_id" => order.id},
shipping_address_collection: %{
allowed_countries: ["GB", "US", "CA", "AU", "DE", "FR", "NL", "IE", "AT", "BE"]
}
}
|> maybe_add_shipping_options(hydrated_items)
|> maybe_add_cart_recovery_notice()
case Stripe.Checkout.Session.create(params) do
{:ok, session} ->
{:ok, _order} = Orders.set_stripe_session(order, session.id)
conn
|> redirect(external: session.url)
{:error, %Stripe.Error{message: message}} ->
Logger.error("Stripe session creation failed: #{message}")
Orders.mark_failed(order)
conn
|> put_flash(:error, "Payment setup failed. Please try again.")
|> redirect(to: ~p"/cart")
{:error, reason} ->
Logger.error("Stripe session creation failed: #{inspect(reason)}")
Orders.mark_failed(order)
conn
|> put_flash(:error, "Payment setup failed. Please try again.")
|> redirect(to: ~p"/cart")
end
end
defp maybe_add_cart_recovery_notice(params) do
if Berrypod.Settings.abandoned_cart_recovery_enabled?() do
Map.put(params, :custom_text, %{
after_submit: %{
message:
"If your payment doesn't complete, we may send you one follow-up email. You can unsubscribe at any time."
}
})
else
params
end
end
defp maybe_add_shipping_options(params, hydrated_items) do
gb_result = Shipping.calculate_for_cart(hydrated_items, "GB")
us_result = Shipping.calculate_for_cart(hydrated_items, "US")
options =
[]
|> maybe_add_option(gb_result, "UK delivery", 5, 10)
|> maybe_add_option(us_result, "International delivery", 10, 20)
if options == [] do
params
else
Map.put(params, :shipping_options, options)
end
end
defp maybe_add_option(options, {:ok, cost}, name, min_days, max_days) when cost > 0 do
option = %{
shipping_rate_data: %{
type: "fixed_amount",
display_name: name,
fixed_amount: %{amount: cost, currency: "gbp"},
delivery_estimate: %{
minimum: %{unit: "business_day", value: min_days},
maximum: %{unit: "business_day", value: max_days}
}
}
}
options ++ [option]
end
defp maybe_add_option(options, _result, _name, _min, _max), do: options
defp track_checkout_start(conn) do
visitor_hash = get_session(conn, "analytics_visitor_hash")
if visitor_hash do
Analytics.track_event("checkout_start", %{
pathname: "/checkout",
visitor_hash: visitor_hash,
browser: get_session(conn, "analytics_browser"),
os: get_session(conn, "analytics_os"),
screen_size: get_session(conn, "analytics_screen_size"),
country_code: get_session(conn, "country_code")
})
end
end
end