defmodule SimpleshopTheme.Providers.Printify do @moduledoc """ Printify provider implementation. Handles product sync and order submission for Printify. """ @behaviour SimpleshopTheme.Providers.Provider alias SimpleshopTheme.Clients.Printify, as: Client alias SimpleshopTheme.Products.ProviderConnection @impl true def provider_type, do: "printify" @impl true def test_connection(%ProviderConnection{} = conn) do with api_key when is_binary(api_key) <- ProviderConnection.get_api_key(conn), :ok <- set_api_key(api_key), {:ok, shops} <- Client.get_shops() do shop = List.first(shops) {:ok, %{ shop_id: shop["id"], shop_name: shop["title"], shop_count: length(shops) }} else nil -> {:error, :no_api_key} {:error, _} = error -> error end end @impl true def fetch_products(%ProviderConnection{config: config} = conn) do shop_id = config["shop_id"] if is_nil(shop_id) do {:error, :no_shop_id} else with api_key when is_binary(api_key) <- ProviderConnection.get_api_key(conn), :ok <- set_api_key(api_key), {:ok, products} <- fetch_all_products(shop_id) do {:ok, products} else nil -> {:error, :no_api_key} {:error, _} = error -> error end end end # Fetches all products by paginating through the API defp fetch_all_products(shop_id) do fetch_products_page(shop_id, 1, []) end defp fetch_products_page(shop_id, page, acc) do case Client.list_products(shop_id, page: page) do {:ok, response} -> products = Enum.map(response["data"] || [], &normalize_product/1) all_products = acc ++ products current_page = response["current_page"] || page last_page = response["last_page"] || 1 if current_page < last_page do # Small delay to be nice to rate limits (600/min = 10/sec) Process.sleep(100) fetch_products_page(shop_id, page + 1, all_products) else {:ok, all_products} end {:error, _} = error -> error end end @impl true def submit_order(%ProviderConnection{config: config} = conn, order) do shop_id = config["shop_id"] if is_nil(shop_id) do {:error, :no_shop_id} else with api_key when is_binary(api_key) <- ProviderConnection.get_api_key(conn), :ok <- set_api_key(api_key), order_data <- build_order_payload(order), {:ok, response} <- Client.create_order(shop_id, order_data) do {:ok, %{provider_order_id: response["id"]}} else nil -> {:error, :no_api_key} {:error, _} = error -> error end end end @impl true def get_order_status(%ProviderConnection{config: config} = conn, provider_order_id) do shop_id = config["shop_id"] if is_nil(shop_id) do {:error, :no_shop_id} else with api_key when is_binary(api_key) <- ProviderConnection.get_api_key(conn), :ok <- set_api_key(api_key), {:ok, response} <- Client.get_order(shop_id, provider_order_id) do {:ok, normalize_order_status(response)} else nil -> {:error, :no_api_key} {:error, _} = error -> error end end end # ============================================================================= # Webhook Registration # ============================================================================= @webhook_events ["product:updated", "product:deleted", "product:publish:started"] @doc """ Registers webhooks for product events with Printify. Returns {:ok, results} or {:error, reason}. """ def register_webhooks(%ProviderConnection{config: config} = conn, webhook_url) do shop_id = config["shop_id"] secret = config["webhook_secret"] cond do is_nil(shop_id) -> {:error, :no_shop_id} is_nil(secret) or secret == "" -> {:error, :no_webhook_secret} true -> with api_key when is_binary(api_key) <- ProviderConnection.get_api_key(conn), :ok <- set_api_key(api_key) do results = Enum.map(@webhook_events, fn event -> case Client.create_webhook(shop_id, webhook_url, event, secret) do {:ok, response} -> {:ok, event, response} {:error, reason} -> {:error, event, reason} end end) {:ok, results} else nil -> {:error, :no_api_key} end end end @doc """ Lists registered webhooks for the shop. """ def list_webhooks(%ProviderConnection{config: config} = conn) do shop_id = config["shop_id"] if is_nil(shop_id) do {:error, :no_shop_id} else with api_key when is_binary(api_key) <- ProviderConnection.get_api_key(conn), :ok <- set_api_key(api_key), {:ok, webhooks} <- Client.list_webhooks(shop_id) do {:ok, webhooks} else nil -> {:error, :no_api_key} {:error, _} = error -> error end end end # ============================================================================= # Data Normalization # ============================================================================= defp normalize_product(raw) do %{ provider_product_id: to_string(raw["id"]), title: raw["title"], description: raw["description"], category: extract_category(raw), images: normalize_images(raw["images"] || []), variants: normalize_variants(raw["variants"] || []), provider_data: %{ blueprint_id: raw["blueprint_id"], print_provider_id: raw["print_provider_id"], tags: raw["tags"] || [], options: raw["options"] || [], raw: raw } } end defp normalize_images(images) do images |> Enum.with_index() |> Enum.map(fn {img, index} -> %{ src: img["src"], position: img["position"] || index, alt: nil } end) end defp normalize_variants(variants) do Enum.map(variants, fn var -> %{ provider_variant_id: to_string(var["id"]), title: var["title"], sku: var["sku"], price: var["price"], cost: var["cost"], options: normalize_variant_options(var), is_enabled: var["is_enabled"] == true, is_available: var["is_available"] == true } end) end defp normalize_variant_options(variant) do # Printify variants have options as a list of option value IDs # We need to build the human-readable map from the variant title # Format: "Size / Color" -> %{"Size" => "Large", "Color" => "Blue"} title = variant["title"] || "" parts = String.split(title, " / ") # Common option names based on position option_names = ["Size", "Color", "Style"] parts |> Enum.with_index() |> Enum.reduce(%{}, fn {value, index}, acc -> key = Enum.at(option_names, index) || "Option #{index + 1}" Map.put(acc, key, value) end) end defp extract_category(raw) do # Try to extract category from tags tags = raw["tags"] || [] cond do "apparel" in tags or "clothing" in tags -> "Apparel" "homeware" in tags or "home" in tags -> "Homewares" "accessories" in tags -> "Accessories" "art" in tags or "print" in tags -> "Art Prints" true -> nil end end defp normalize_order_status(raw) do %{ status: map_order_status(raw["status"]), provider_status: raw["status"], tracking_number: extract_tracking(raw), tracking_url: extract_tracking_url(raw), shipments: raw["shipments"] || [] } end defp map_order_status("pending"), do: "pending" defp map_order_status("on-hold"), do: "pending" defp map_order_status("payment-not-received"), do: "pending" defp map_order_status("in-production"), do: "processing" defp map_order_status("partially-shipped"), do: "processing" defp map_order_status("shipped"), do: "shipped" defp map_order_status("delivered"), do: "delivered" defp map_order_status("canceled"), do: "cancelled" defp map_order_status(_), do: "pending" defp extract_tracking(raw) do case raw["shipments"] do [shipment | _] -> shipment["tracking_number"] _ -> nil end end defp extract_tracking_url(raw) do case raw["shipments"] do [shipment | _] -> shipment["tracking_url"] _ -> nil end end # ============================================================================= # Order Building # ============================================================================= defp build_order_payload(order) do %{ external_id: order.order_number, label: order.order_number, line_items: Enum.map(order.line_items, fn item -> %{ product_id: item.product_variant.product.provider_product_id, variant_id: String.to_integer(item.product_variant.provider_variant_id), quantity: item.quantity } end), shipping_method: 1, address_to: %{ first_name: order.shipping_address["first_name"], last_name: order.shipping_address["last_name"], email: order.customer_email, phone: order.shipping_address["phone"], country: order.shipping_address["country"], region: order.shipping_address["state"] || order.shipping_address["region"], address1: order.shipping_address["address1"], address2: order.shipping_address["address2"], city: order.shipping_address["city"], zip: order.shipping_address["zip"] || order.shipping_address["postal_code"] } } end # ============================================================================= # API Key Management # ============================================================================= # Temporarily sets the API key for the request # In a production system, this would use a connection pool or request context defp set_api_key(api_key) do Process.put(:printify_api_key, api_key) :ok end end