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: {:ok, SimpleshopTheme.Providers.Printful} 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