All checks were successful
deploy / deploy (push) Successful in 1m26s
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>
166 lines
4.7 KiB
Elixir
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
|