berrypod/lib/simpleshop_theme_web/plugs/country_detect.ex
jamey 5c2f70ce44 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>
2026-02-14 10:48:00 +00:00

56 lines
1.5 KiB
Elixir

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