berrypod/lib/simpleshop_theme/clients/printful.ex

319 lines
8.6 KiB
Elixir
Raw Normal View History

defmodule SimpleshopTheme.Clients.Printful do
@moduledoc """
HTTP client for the Printful API.
Uses v2 endpoints where available, v1 for sync products (store products).
Handles authentication via Bearer tokens stored in the process dictionary.
"""
@base_url "https://api.printful.com"
# =============================================================================
# Auth
# =============================================================================
@doc """
Get the API token.
Checks process dictionary first (for provider connections with stored credentials),
then falls back to environment variable (for development/testing).
"""
def api_token do
Process.get(:printful_api_key) ||
System.get_env("PRINTFUL_API_TOKEN") ||
raise "PRINTFUL_API_TOKEN environment variable is not set"
end
@doc """
Get the store ID from the process dictionary.
"""
def store_id do
Process.get(:printful_store_id)
end
# =============================================================================
# Core HTTP
# =============================================================================
@doc """
Make a GET request to the Printful 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, unwrap_response(path, body)}
{:ok, %Req.Response{status: status, body: body}} ->
{:error, {status, body}}
{:error, reason} ->
{:error, reason}
end
end
@doc """
Make a POST request to the Printful 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, unwrap_response(path, body)}
{:ok, %Req.Response{status: status, body: body}} ->
{:error, {status, body}}
{:error, reason} ->
{:error, reason}
end
end
@doc """
Make a DELETE request to the Printful 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, unwrap_response(path, body)}
{:ok, %Req.Response{status: 204}} ->
{:ok, nil}
{:ok, %Req.Response{status: status, body: body}} ->
{:error, {status, body}}
{:error, reason} ->
{:error, reason}
end
end
# v1 responses wrap data in "result", v2 wraps in "data"
defp unwrap_response("/v2/" <> _, %{"data" => data}), do: data
defp unwrap_response(_path, %{"code" => _, "result" => result}), do: result
defp unwrap_response(_path, body), do: body
# =============================================================================
# Stores (v2)
# =============================================================================
@doc """
List all stores for the authenticated account.
"""
def get_stores do
get("/v2/stores")
end
@doc """
Get the first store's ID.
"""
def get_store_id do
case get_stores() do
{:ok, stores} when is_list(stores) and stores != [] ->
{:ok, hd(stores)["id"]}
{:ok, []} ->
{:error, :no_stores}
error ->
error
end
end
# =============================================================================
# Catalogue (v2)
# =============================================================================
@doc """
Get a catalogue product by ID.
"""
def get_catalog_product(product_id) do
get("/v2/catalog-products/#{product_id}")
end
@doc """
Get variants for a catalogue product.
"""
def get_catalog_variants(product_id) do
get("/v2/catalog-products/#{product_id}/catalog-variants")
end
@doc """
Get stock availability for a catalogue product.
"""
def get_product_availability(product_id) do
get("/v2/catalog-products/#{product_id}/availability")
end
# =============================================================================
# Sync Products (v1 — no v2 equivalent)
# =============================================================================
@doc """
List the seller's configured products (sync products).
These are products the seller has set up in Printful's dashboard with designs.
Supports pagination via `offset` and `limit` options.
"""
def list_sync_products(opts \\ []) do
offset = Keyword.get(opts, :offset, 0)
limit = Keyword.get(opts, :limit, 20)
get("/store/products?offset=#{offset}&limit=#{limit}")
end
@doc """
Get a single sync product with all its variants and files.
"""
def get_sync_product(product_id) do
get("/store/products/#{product_id}")
end
# =============================================================================
# Shipping (v2)
# =============================================================================
@doc """
Calculate shipping rates for a set of items to a recipient.
## Example
calculate_shipping(
%{country_code: "GB"},
[%{source: "catalog", catalog_variant_id: 474, quantity: 1}]
)
"""
def calculate_shipping(recipient, items) do
post("/v2/shipping-rates", %{recipient: recipient, items: items})
end
# =============================================================================
# Orders (v2)
# =============================================================================
@doc """
Create a new order (draft status).
"""
def create_order(order_data) do
post("/v2/orders", order_data)
end
@doc """
Confirm an order for fulfilment. This triggers charges and production.
"""
def confirm_order(order_id) do
post("/v2/orders/#{order_id}/confirmation", %{})
end
@doc """
Get an order by ID.
"""
def get_order(order_id) do
get("/v2/orders/#{order_id}")
end
@doc """
Get shipments for an order.
"""
def get_order_shipments(order_id) do
get("/v2/orders/#{order_id}/shipments")
end
# =============================================================================
# Mockups (v2)
# =============================================================================
@doc """
Create a mockup generation task.
"""
def create_mockup_task(body) do
post("/v2/mockup-tasks", body)
end
@doc """
Get mockup task results. Pass `task_id` to poll a specific task.
"""
def get_mockup_tasks(params \\ %{}) do
query = URI.encode_query(params)
path = if query == "", do: "/v2/mockup-tasks", else: "/v2/mockup-tasks?#{query}"
get(path)
end
# =============================================================================
# Files (v2)
# =============================================================================
@doc """
Upload a file to the Printful file library.
"""
def upload_file(url) do
post("/v2/files", %{url: url})
end
# =============================================================================
# Webhooks (v2)
# =============================================================================
@doc """
Set up webhook configuration.
"""
def setup_webhooks(url, events) do
post("/v2/webhooks", %{url: url, events: events})
end
@doc """
Get current webhook configuration.
"""
def get_webhooks do
get("/v2/webhooks")
end
@doc """
Disable webhook support.
"""
def delete_webhooks do
delete("/v2/webhooks")
end
# =============================================================================
# File Downloads
# =============================================================================
@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
# =============================================================================
# Private
# =============================================================================
defp auth_headers do
headers = [
{"Authorization", "Bearer #{api_token()}"},
{"Content-Type", "application/json"}
]
case store_id() do
nil -> headers
id -> [{"X-PF-Store-Id", to_string(id)} | headers]
end
end
end