feat: add Products context with provider integration (Phase 1)
Implement the schema foundation for syncing products from POD providers like Printify. This includes encrypted credential storage, product/variant schemas, and an Oban worker for background sync. New modules: - Vault: AES-256-GCM encryption for API keys - Products context: CRUD and sync operations for products - Provider behaviour: abstraction for POD provider implementations - ProductSyncWorker: Oban job for async product sync Schemas: ProviderConnection, Product, ProductImage, ProductVariant Also reorganizes Printify client to lib/simpleshop_theme/clients/ and mockup generator to lib/simpleshop_theme/mockups/ for better structure. 134 tests added covering all new functionality. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
209
lib/simpleshop_theme/clients/printify.ex
Normal file
209
lib/simpleshop_theme/clients/printify.ex
Normal file
@@ -0,0 +1,209 @@
|
||||
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 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 length(shops) > 0 ->
|
||||
{: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.
|
||||
"""
|
||||
def list_products(shop_id, opts \\ []) do
|
||||
limit = Keyword.get(opts, :limit, 100)
|
||||
page = Keyword.get(opts, :page, 1)
|
||||
get("/shops/#{shop_id}/products.json?limit=#{limit}&page=#{page}")
|
||||
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
|
||||
|
||||
@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
|
||||
Reference in New Issue
Block a user