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:
55
lib/simpleshop_theme_web/plugs/country_detect.ex
Normal file
55
lib/simpleshop_theme_web/plugs/country_detect.ex
Normal file
@@ -0,0 +1,55 @@
|
||||
defmodule SimpleshopThemeWeb.Plugs.CountryDetect do
|
||||
@moduledoc """
|
||||
Plug that detects the visitor's country from cookies or Accept-Language.
|
||||
|
||||
Priority:
|
||||
1. `shipping_country` cookie (set when user explicitly changes country)
|
||||
2. Accept-Language header (locale tags like `en-GB` → `GB`)
|
||||
3. Falls back to `"GB"`
|
||||
|
||||
The result is stored in the session as `country_code` so LiveViews can
|
||||
read it. Only runs once per session — skips if `country_code` is already
|
||||
set (unless the cookie has changed).
|
||||
"""
|
||||
|
||||
import Plug.Conn
|
||||
|
||||
@default_country "GB"
|
||||
@cookie_name "shipping_country"
|
||||
|
||||
def init(opts), do: opts
|
||||
|
||||
def call(conn, _opts) do
|
||||
cookie_country = conn.cookies[@cookie_name]
|
||||
session_country = get_session(conn, "country_code")
|
||||
|
||||
cond do
|
||||
# Cookie takes priority — user explicitly chose this country
|
||||
cookie_country not in [nil, ""] and cookie_country != session_country ->
|
||||
put_session(conn, "country_code", cookie_country)
|
||||
|
||||
# Session already set and no cookie override
|
||||
session_country != nil ->
|
||||
conn
|
||||
|
||||
# First visit: detect from Accept-Language
|
||||
true ->
|
||||
country = detect_from_header(conn)
|
||||
put_session(conn, "country_code", country)
|
||||
end
|
||||
end
|
||||
|
||||
defp detect_from_header(conn) do
|
||||
conn
|
||||
|> get_req_header("accept-language")
|
||||
|> List.first("")
|
||||
|> parse_country()
|
||||
end
|
||||
|
||||
defp parse_country(header) do
|
||||
case Regex.run(~r/[a-z]{2}-([A-Z]{2})/, header) do
|
||||
[_, country] -> country
|
||||
nil -> @default_country
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user