berrypod/lib/simpleshop_theme/clients/printify.ex
jamey daa6d3de71 add per-colour product images and gallery colour filtering
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>
2026-02-15 23:21:22 +00:00

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