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>
107 lines
3.5 KiB
Elixir
107 lines
3.5 KiB
Elixir
defmodule SimpleshopTheme.Providers.Provider do
|
|
@moduledoc """
|
|
Behaviour for POD provider integrations.
|
|
|
|
Each provider (Printify, Gelato, Prodigi, etc.) implements this behaviour
|
|
to provide a consistent interface for:
|
|
|
|
- Testing connections
|
|
- Fetching products
|
|
- Submitting orders
|
|
- Tracking order status
|
|
|
|
## Data Normalization
|
|
|
|
Providers return normalized data structures:
|
|
|
|
- Products are maps with keys: `title`, `description`, `provider_product_id`,
|
|
`images`, `variants`, `category`, `provider_data`
|
|
- Variants are maps with keys: `provider_variant_id`, `title`, `sku`, `price`,
|
|
`cost`, `options`, `is_enabled`, `is_available`
|
|
- Images are maps with keys: `src`, `position`, `alt`
|
|
"""
|
|
|
|
alias SimpleshopTheme.Products.ProviderConnection
|
|
|
|
@doc """
|
|
Returns the provider type identifier (e.g., "printify", "gelato").
|
|
"""
|
|
@callback provider_type() :: String.t()
|
|
|
|
@doc """
|
|
Tests the connection to the provider.
|
|
|
|
Returns `{:ok, info}` with provider-specific info (e.g., shop name)
|
|
or `{:error, reason}` if the connection fails.
|
|
"""
|
|
@callback test_connection(ProviderConnection.t()) :: {:ok, map()} | {:error, term()}
|
|
|
|
@doc """
|
|
Fetches all products from the provider.
|
|
|
|
Returns a list of normalized product maps.
|
|
"""
|
|
@callback fetch_products(ProviderConnection.t()) :: {:ok, [map()]} | {:error, term()}
|
|
|
|
@doc """
|
|
Submits an order to the provider for fulfillment.
|
|
|
|
Returns `{:ok, %{provider_order_id: String.t()}}` on success.
|
|
"""
|
|
@callback submit_order(ProviderConnection.t(), order :: map()) ::
|
|
{:ok, %{provider_order_id: String.t()}} | {:error, term()}
|
|
|
|
@doc """
|
|
Gets the current status of an order from the provider.
|
|
"""
|
|
@callback get_order_status(ProviderConnection.t(), provider_order_id :: String.t()) ::
|
|
{:ok, map()} | {:error, term()}
|
|
|
|
@doc """
|
|
Fetches shipping rates from the provider for the given products.
|
|
|
|
Takes the connection and the already-fetched product list (from fetch_products).
|
|
Returns normalized rate maps with keys: blueprint_id, print_provider_id,
|
|
country_code, first_item_cost, additional_item_cost, currency, handling_time_days.
|
|
|
|
Optional — providers that don't support shipping rate lookup can skip this.
|
|
The sync worker checks `function_exported?/3` before calling.
|
|
"""
|
|
@callback fetch_shipping_rates(ProviderConnection.t(), products :: [map()]) ::
|
|
{:ok, [map()]} | {:error, term()}
|
|
|
|
@optional_callbacks [fetch_shipping_rates: 2]
|
|
|
|
@doc """
|
|
Returns the provider module for a given provider type.
|
|
|
|
Checks `:provider_modules` application config first, allowing test
|
|
overrides via Mox. Falls back to hardcoded dispatch.
|
|
"""
|
|
def for_type(type) do
|
|
case Application.get_env(:simpleshop_theme, :provider_modules, %{}) do
|
|
modules when is_map(modules) ->
|
|
case Map.get(modules, type) do
|
|
nil -> default_for_type(type)
|
|
module -> {:ok, module}
|
|
end
|
|
|
|
_ ->
|
|
default_for_type(type)
|
|
end
|
|
end
|
|
|
|
defp default_for_type("printify"), do: {:ok, SimpleshopTheme.Providers.Printify}
|
|
defp default_for_type("gelato"), do: {:error, :not_implemented}
|
|
defp default_for_type("prodigi"), do: {:error, :not_implemented}
|
|
defp default_for_type("printful"), do: {:error, :not_implemented}
|
|
defp default_for_type(type), do: {:error, {:unknown_provider, type}}
|
|
|
|
@doc """
|
|
Returns the provider module for a provider connection.
|
|
"""
|
|
def for_connection(%ProviderConnection{provider_type: type}) do
|
|
for_type(type)
|
|
end
|
|
end
|