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,53 @@
defmodule SimpleshopTheme.Shipping.ShippingRate do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "shipping_rates" do
field :blueprint_id, :integer
field :print_provider_id, :integer
field :country_code, :string
field :first_item_cost, :integer
field :additional_item_cost, :integer
field :currency, :string, default: "USD"
field :handling_time_days, :integer
belongs_to :provider_connection, SimpleshopTheme.Products.ProviderConnection
timestamps(type: :utc_datetime)
end
def changeset(rate, attrs) do
rate
|> cast(attrs, [
:provider_connection_id,
:blueprint_id,
:print_provider_id,
:country_code,
:first_item_cost,
:additional_item_cost,
:currency,
:handling_time_days
])
|> validate_required([
:provider_connection_id,
:blueprint_id,
:print_provider_id,
:country_code,
:first_item_cost,
:additional_item_cost,
:currency
])
|> validate_number(:first_item_cost, greater_than_or_equal_to: 0)
|> validate_number(:additional_item_cost, greater_than_or_equal_to: 0)
|> foreign_key_constraint(:provider_connection_id)
|> unique_constraint([
:provider_connection_id,
:blueprint_id,
:print_provider_id,
:country_code
])
end
end