add shipping costs with live exchange rates and country detection
Shipping rates fetched from Printify during product sync, converted to GBP at sync time using frankfurter.app ECB exchange rates with 5% buffer. Cached in shipping_rates table per blueprint/provider/country. Cart page shows shipping estimate with country selector (detected from Accept-Language header, persisted in cookie). Stripe Checkout includes shipping_options for UK domestic and international delivery. Order shipping_cost extracted from Stripe on payment. ScheduledSyncWorker runs every 6 hours via Oban cron to keep rates and exchange rates fresh. REST_OF_THE_WORLD fallback covers unlisted countries. 780 tests, 0 failures. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ defmodule SimpleshopThemeWeb.CheckoutController do
|
||||
|
||||
alias SimpleshopTheme.Cart
|
||||
alias SimpleshopTheme.Orders
|
||||
alias SimpleshopTheme.Shipping
|
||||
|
||||
require Logger
|
||||
|
||||
@@ -54,16 +55,18 @@ defmodule SimpleshopThemeWeb.CheckoutController do
|
||||
|
||||
base_url = SimpleshopThemeWeb.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"]
|
||||
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)
|
||||
|
||||
case Stripe.Checkout.Session.create(params) do
|
||||
{:ok, session} ->
|
||||
@@ -89,4 +92,38 @@ defmodule SimpleshopThemeWeb.CheckoutController do
|
||||
|> redirect(to: ~p"/cart")
|
||||
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
|
||||
end
|
||||
|
||||
@@ -36,6 +36,9 @@ defmodule SimpleshopThemeWeb.StripeWebhookController do
|
||||
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
|
||||
@@ -111,4 +114,19 @@ defmodule SimpleshopThemeWeb.StripeWebhookController do
|
||||
|
||||
Orders.update_order(order, %{shipping_address: shipping_address})
|
||||
end
|
||||
|
||||
defp update_shipping_cost(order, session) do
|
||||
shipping_amount = get_in(session, [Access.key(:shipping_cost), Access.key(: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
|
||||
|
||||
Reference in New Issue
Block a user