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:
jamey
2026-02-14 10:48:00 +00:00
parent 44933acebb
commit 5c2f70ce44
26 changed files with 1707 additions and 38 deletions

View File

@@ -0,0 +1,99 @@
defmodule SimpleshopThemeWeb.Plugs.CountryDetectTest do
use SimpleshopThemeWeb.ConnCase, async: true
alias SimpleshopThemeWeb.Plugs.CountryDetect
defp with_cookies(conn) do
Plug.Conn.fetch_cookies(conn)
end
describe "call/2" do
test "detects GB from en-GB header", %{conn: conn} do
conn =
conn
|> init_test_session(%{})
|> with_cookies()
|> put_req_header("accept-language", "en-GB,en;q=0.9")
|> CountryDetect.call([])
assert get_session(conn, "country_code") == "GB"
end
test "detects DE from de-DE header", %{conn: conn} do
conn =
conn
|> init_test_session(%{})
|> with_cookies()
|> put_req_header("accept-language", "de-DE,de;q=0.9,en;q=0.8")
|> CountryDetect.call([])
assert get_session(conn, "country_code") == "DE"
end
test "detects FR from fr-FR header", %{conn: conn} do
conn =
conn
|> init_test_session(%{})
|> with_cookies()
|> put_req_header("accept-language", "fr-FR,fr;q=0.9")
|> CountryDetect.call([])
assert get_session(conn, "country_code") == "FR"
end
test "defaults to GB when no country in header", %{conn: conn} do
conn =
conn
|> init_test_session(%{})
|> with_cookies()
|> put_req_header("accept-language", "en;q=0.9")
|> CountryDetect.call([])
assert get_session(conn, "country_code") == "GB"
end
test "defaults to GB when no Accept-Language header", %{conn: conn} do
conn =
conn
|> init_test_session(%{})
|> with_cookies()
|> CountryDetect.call([])
assert get_session(conn, "country_code") == "GB"
end
test "does not overwrite existing country_code in session", %{conn: conn} do
conn =
conn
|> init_test_session(%{"country_code" => "US"})
|> with_cookies()
|> put_req_header("accept-language", "en-GB,en;q=0.9")
|> CountryDetect.call([])
assert get_session(conn, "country_code") == "US"
end
test "cookie overrides session country", %{conn: conn} do
conn =
conn
|> init_test_session(%{"country_code" => "GB"})
|> put_req_cookie("shipping_country", "DE")
|> with_cookies()
|> CountryDetect.call([])
assert get_session(conn, "country_code") == "DE"
end
test "cookie overrides Accept-Language detection", %{conn: conn} do
conn =
conn
|> init_test_session(%{})
|> put_req_cookie("shipping_country", "US")
|> with_cookies()
|> put_req_header("accept-language", "en-GB,en;q=0.9")
|> CountryDetect.call([])
assert get_session(conn, "country_code") == "US"
end
end
end