Tag product images with their colour during sync (both Printful and Printify providers). Printify images are cherry-picked: hero colour keeps all angles, other colours keep front + back only. Printful MockupEnricher now generates mockups per colour from the color_variant_map. PDP gallery filters by the selected colour, falling back to all images when the selected colour has none. Fix option name mismatch (Printify "Colors" vs variant "Color") by singularizing in Product.option_types. Generator creates multi-colour apparel products so mock data matches real sync behaviour. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
271 lines
6.5 KiB
Elixir
271 lines
6.5 KiB
Elixir
defmodule SimpleshopTheme.Clients.Printify do
|
|
@moduledoc """
|
|
HTTP client for the Printify API.
|
|
|
|
Handles authentication and provides low-level API access.
|
|
Requires PRINTIFY_API_TOKEN environment variable to be set.
|
|
"""
|
|
|
|
@base_url "https://api.printify.com/v1"
|
|
|
|
@doc """
|
|
Get the API token.
|
|
|
|
Checks process dictionary first (for provider connections with stored credentials),
|
|
then falls back to environment variable (for development/mockup generation).
|
|
"""
|
|
def api_token do
|
|
Process.get(:printify_api_key) ||
|
|
System.get_env("PRINTIFY_API_TOKEN") ||
|
|
raise "PRINTIFY_API_TOKEN environment variable is not set"
|
|
end
|
|
|
|
@doc """
|
|
Make a GET request to the Printify API.
|
|
"""
|
|
def get(path, _opts \\ []) do
|
|
url = @base_url <> path
|
|
|
|
case Req.get(url, headers: auth_headers(), receive_timeout: 30_000) do
|
|
{:ok, %Req.Response{status: status, body: body}} when status in 200..299 ->
|
|
{:ok, body}
|
|
|
|
{:ok, %Req.Response{status: status, body: body}} ->
|
|
{:error, {status, body}}
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Make a POST request to the Printify API.
|
|
"""
|
|
def post(path, body, _opts \\ []) do
|
|
url = @base_url <> path
|
|
|
|
case Req.post(url, json: body, headers: auth_headers(), receive_timeout: 60_000) do
|
|
{:ok, %Req.Response{status: status, body: body}} when status in 200..299 ->
|
|
{:ok, body}
|
|
|
|
{:ok, %Req.Response{status: status, body: body}} ->
|
|
{:error, {status, body}}
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Make a PUT request to the Printify API.
|
|
"""
|
|
def put(path, body, _opts \\ []) do
|
|
url = @base_url <> path
|
|
|
|
case Req.put(url, json: body, headers: auth_headers(), receive_timeout: 60_000) do
|
|
{:ok, %Req.Response{status: status, body: body}} when status in 200..299 ->
|
|
{:ok, body}
|
|
|
|
{:ok, %Req.Response{status: status, body: body}} ->
|
|
{:error, {status, body}}
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Make a DELETE request to the Printify API.
|
|
"""
|
|
def delete(path, _opts \\ []) do
|
|
url = @base_url <> path
|
|
|
|
case Req.delete(url, headers: auth_headers(), receive_timeout: 30_000) do
|
|
{:ok, %Req.Response{status: status, body: body}} when status in 200..299 ->
|
|
{:ok, body}
|
|
|
|
{:ok, %Req.Response{status: status}} when status == 204 ->
|
|
{:ok, nil}
|
|
|
|
{:ok, %Req.Response{status: status, body: body}} ->
|
|
{:error, {status, body}}
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Get all shops for the authenticated account.
|
|
"""
|
|
def get_shops do
|
|
get("/shops.json")
|
|
end
|
|
|
|
@doc """
|
|
Get the first shop ID for the account.
|
|
"""
|
|
def get_shop_id do
|
|
case get_shops() do
|
|
{:ok, shops} when is_list(shops) and shops != [] ->
|
|
{:ok, hd(shops)["id"]}
|
|
|
|
{:ok, []} ->
|
|
{:error, :no_shops}
|
|
|
|
error ->
|
|
error
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Get all blueprints (product types) from the catalog.
|
|
"""
|
|
def get_blueprints do
|
|
get("/catalog/blueprints.json")
|
|
end
|
|
|
|
@doc """
|
|
Get print providers for a specific blueprint.
|
|
"""
|
|
def get_print_providers(blueprint_id) do
|
|
get("/catalog/blueprints/#{blueprint_id}/print_providers.json")
|
|
end
|
|
|
|
@doc """
|
|
Get variants for a specific blueprint and print provider.
|
|
"""
|
|
def get_variants(blueprint_id, print_provider_id) do
|
|
get("/catalog/blueprints/#{blueprint_id}/print_providers/#{print_provider_id}/variants.json")
|
|
end
|
|
|
|
@doc """
|
|
Get shipping information for a blueprint/provider combination.
|
|
"""
|
|
def get_shipping(blueprint_id, print_provider_id) do
|
|
get("/catalog/blueprints/#{blueprint_id}/print_providers/#{print_provider_id}/shipping.json")
|
|
end
|
|
|
|
@doc """
|
|
Upload an image to Printify via URL.
|
|
"""
|
|
def upload_image(file_name, url) do
|
|
post("/uploads/images.json", %{
|
|
file_name: file_name,
|
|
url: url
|
|
})
|
|
end
|
|
|
|
@doc """
|
|
Create a product in a shop.
|
|
"""
|
|
def create_product(shop_id, product_data) do
|
|
post("/shops/#{shop_id}/products.json", product_data)
|
|
end
|
|
|
|
@doc """
|
|
Get a product by ID.
|
|
"""
|
|
def get_product(shop_id, product_id) do
|
|
get("/shops/#{shop_id}/products/#{product_id}.json")
|
|
end
|
|
|
|
@doc """
|
|
List all products in a shop.
|
|
Printify allows a maximum of 50 products per page.
|
|
"""
|
|
def list_products(shop_id, opts \\ []) do
|
|
limit = Keyword.get(opts, :limit, 50)
|
|
page = Keyword.get(opts, :page, 1)
|
|
get("/shops/#{shop_id}/products.json?limit=#{limit}&page=#{page}")
|
|
end
|
|
|
|
@doc """
|
|
Update a product in a shop.
|
|
"""
|
|
def update_product(shop_id, product_id, product_data) do
|
|
put("/shops/#{shop_id}/products/#{product_id}.json", product_data)
|
|
end
|
|
|
|
@doc """
|
|
Delete a product from a shop.
|
|
"""
|
|
def delete_product(shop_id, product_id) do
|
|
delete("/shops/#{shop_id}/products/#{product_id}.json")
|
|
end
|
|
|
|
@doc """
|
|
Create an order in a shop.
|
|
"""
|
|
def create_order(shop_id, order_data) do
|
|
post("/shops/#{shop_id}/orders.json", order_data)
|
|
end
|
|
|
|
@doc """
|
|
Get an order by ID.
|
|
"""
|
|
def get_order(shop_id, order_id) do
|
|
get("/shops/#{shop_id}/orders/#{order_id}.json")
|
|
end
|
|
|
|
# =============================================================================
|
|
# Webhooks
|
|
# =============================================================================
|
|
|
|
@doc """
|
|
Register a webhook with Printify.
|
|
|
|
## Event types
|
|
- "product:publish:started"
|
|
- "product:updated"
|
|
- "product:deleted"
|
|
- "shop:disconnected"
|
|
"""
|
|
def create_webhook(shop_id, url, topic, secret) do
|
|
post("/shops/#{shop_id}/webhooks.json", %{
|
|
topic: topic,
|
|
url: url,
|
|
secret: secret
|
|
})
|
|
end
|
|
|
|
@doc """
|
|
List registered webhooks for a shop.
|
|
"""
|
|
def list_webhooks(shop_id) do
|
|
get("/shops/#{shop_id}/webhooks.json")
|
|
end
|
|
|
|
@doc """
|
|
Delete a webhook.
|
|
"""
|
|
def delete_webhook(shop_id, webhook_id) do
|
|
delete("/shops/#{shop_id}/webhooks/#{webhook_id}.json")
|
|
end
|
|
|
|
@doc """
|
|
Download a file from a URL to a local path.
|
|
"""
|
|
def download_file(url, output_path) do
|
|
case Req.get(url, into: File.stream!(output_path), receive_timeout: 60_000) do
|
|
{:ok, %Req.Response{status: status}} when status in 200..299 ->
|
|
{:ok, output_path}
|
|
|
|
{:ok, %Req.Response{status: status}} ->
|
|
File.rm(output_path)
|
|
{:error, {:http_error, status}}
|
|
|
|
{:error, reason} ->
|
|
File.rm(output_path)
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
defp auth_headers do
|
|
[
|
|
{"Authorization", "Bearer #{api_token()}"},
|
|
{"Content-Type", "application/json"}
|
|
]
|
|
end
|
|
end
|