- new /setup page with three-section onboarding (account, provider, payments) - dashboard launch checklist with progress bar, go-live, dismiss - provider registry on Provider module (single source of truth for metadata) - payments registry for Stripe - setup context made provider-agnostic (provider_connected, theme_customised, etc.) - admin provider pages now fully registry-driven (no hardcoded provider names) - auth flow: fresh installs redirect to /setup, signed_in_path respects setup state - removed old /admin/setup wizard - 840 tests, 0 failures Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
161 lines
5.5 KiB
Elixir
161 lines
5.5 KiB
Elixir
defmodule Berrypod.Providers.Provider do
|
|
@moduledoc """
|
|
Behaviour and registry for POD provider integrations.
|
|
|
|
Each provider (Printify, Printful, etc.) implements this behaviour
|
|
to provide a consistent interface for:
|
|
|
|
- Testing connections
|
|
- Fetching products
|
|
- Submitting orders
|
|
- Tracking order status
|
|
|
|
The `@providers` list is the single source of truth for available providers.
|
|
Both the module dispatch (`for_type/1`) and UI metadata (`all/0`, `get/1`)
|
|
are derived from it.
|
|
|
|
## 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 Berrypod.Products.ProviderConnection
|
|
|
|
# Single source of truth for all providers — module dispatch and UI metadata.
|
|
# Add new providers here. Set module: nil for coming-soon entries.
|
|
@providers [
|
|
%{
|
|
type: "printify",
|
|
module: Berrypod.Providers.Printify,
|
|
name: "Printify",
|
|
tagline: "Largest catalog — 800+ products from 90+ print providers",
|
|
features: ["Biggest product selection", "Multi-provider routing", "Competitive pricing"],
|
|
setup_hint: "Get your API token from Printify → Account → Connections",
|
|
setup_url: "https://printify.com/app/account/connections",
|
|
login_url: "https://printify.com/app/auth/login",
|
|
signup_url: "https://printify.com/app/auth/register",
|
|
setup_steps: [
|
|
~s[Click <strong>Account</strong> (top right)],
|
|
~s[Select <strong>Connections</strong> from the dropdown],
|
|
~s[Find <strong>API tokens</strong> and click <strong>Generate</strong>],
|
|
~s[Enter a name (e.g. "My Shop"), keep <strong>all scopes</strong> selected, and click <strong>Generate token</strong>],
|
|
~s[Click <strong>Copy to clipboard</strong> and paste it below]
|
|
],
|
|
status: :available
|
|
},
|
|
%{
|
|
type: "printful",
|
|
module: Berrypod.Providers.Printful,
|
|
name: "Printful",
|
|
tagline: "Premium quality with in-house fulfilment",
|
|
features: ["In-house production", "Warehousing & branding", "Mockup generator"],
|
|
setup_hint: "Get your API token from Printful → Settings → API",
|
|
setup_url: "https://www.printful.com/dashboard/developer/api",
|
|
login_url: "https://www.printful.com/auth/login",
|
|
signup_url: "https://www.printful.com/auth/signup",
|
|
setup_steps: [
|
|
~s[Go to <strong>Settings</strong> → <strong>API access</strong>],
|
|
~s[Click <strong>Create API key</strong>],
|
|
~s[Give it a name and select <strong>all scopes</strong>],
|
|
~s[Copy the token and paste it below]
|
|
],
|
|
status: :available
|
|
},
|
|
%{
|
|
type: "gelato",
|
|
module: nil,
|
|
name: "Gelato",
|
|
tagline: "Local production in 30+ countries",
|
|
status: :coming_soon
|
|
},
|
|
%{
|
|
type: "prodigi",
|
|
module: nil,
|
|
name: "Prodigi",
|
|
tagline: "Fine art and photo products",
|
|
status: :coming_soon
|
|
}
|
|
]
|
|
|
|
# Build a compile-time lookup map for for_type/1
|
|
@type_to_module Map.new(@providers, fn p -> {p.type, p[:module]} end)
|
|
|
|
# ── Callbacks ──
|
|
|
|
@callback provider_type() :: String.t()
|
|
|
|
@callback test_connection(ProviderConnection.t()) :: {:ok, map()} | {:error, term()}
|
|
|
|
@callback fetch_products(ProviderConnection.t()) :: {:ok, [map()]} | {:error, term()}
|
|
|
|
@callback submit_order(ProviderConnection.t(), order :: map()) ::
|
|
{:ok, %{provider_order_id: String.t()}} | {:error, term()}
|
|
|
|
@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.
|
|
|
|
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]
|
|
|
|
# ── Registry ──
|
|
|
|
@doc "Returns all providers (available and coming soon)."
|
|
def all, do: @providers
|
|
|
|
@doc "Returns only providers with `:available` status."
|
|
def available, do: Enum.filter(@providers, &(&1.status == :available))
|
|
|
|
@doc "Returns a provider metadata map by type string, or nil."
|
|
def get(type), do: Enum.find(@providers, &(&1.type == type))
|
|
|
|
# ── Module dispatch ──
|
|
|
|
@doc """
|
|
Returns the provider module for a given provider type.
|
|
|
|
Checks `:provider_modules` application config first, allowing test
|
|
overrides via Mox. Falls back to the `@providers` list.
|
|
"""
|
|
def for_type(type) do
|
|
case Application.get_env(:berrypod, :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(type) do
|
|
case Map.fetch(@type_to_module, type) do
|
|
{:ok, nil} -> {:error, :not_implemented}
|
|
{:ok, module} -> {:ok, module}
|
|
:error -> {:error, {:unknown_provider, type}}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Returns the provider module for a provider connection.
|
|
"""
|
|
def for_connection(%ProviderConnection{provider_type: type}) do
|
|
for_type(type)
|
|
end
|
|
end
|