add Printful mockup generator and post-sync angle enrichment

New PrintfulGenerator module creates demo products in Printful with
multi-variant support (multiple colours/sizes per product type).
Mix task gets --provider flag to choose between Printify and Printful.

After syncing Printful products, MockupEnricher Oban worker calls the
legacy mockup generator API to produce extra angle images (back, left,
right) and appends them as product images. Jobs are staggered 45s apart
with snooze-based 429 handling. Flat products (canvas, poster) get no
extras — apparel and 3D products get 1-5 extra angles each.

Also fixes:
- cross-provider slug uniqueness (appends -2, -3 suffix)
- category mapping order (Accessories before Canvas Prints)
- image dedup by URL instead of colour (fixes canvas variants)
- artwork URL stored in provider_data for enricher access

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-02-15 16:52:53 +00:00
parent 61cb2b7a87
commit 1aceaf9444
7 changed files with 916 additions and 76 deletions

View File

@@ -314,6 +314,7 @@ defmodule SimpleshopTheme.Providers.Printful do
blueprint_id: catalog_product_id,
print_provider_id: 0,
thumbnail_url: sync_product["thumbnail_url"],
artwork_url: extract_artwork_url(sync_variants),
options: build_option_types(sync_variants),
raw: %{sync_product: sync_product}
}
@@ -345,7 +346,7 @@ defmodule SimpleshopTheme.Providers.Printful do
opts
end
# Extract unique preview images from sync variants (one per unique colour)
# Extract unique preview images from sync variants (one per unique image URL)
defp extract_preview_images(sync_variants) do
sync_variants
|> Enum.flat_map(fn sv ->
@@ -354,18 +355,28 @@ defmodule SimpleshopTheme.Providers.Printful do
|> Enum.map(fn file ->
%{
src: file["preview_url"] || file["thumbnail_url"],
color: sv["color"]
color: sv["color"],
name: sv["name"]
}
end)
end)
|> Enum.uniq_by(& &1.color)
|> Enum.uniq_by(& &1.src)
|> Enum.with_index()
|> Enum.map(fn {img, index} ->
%{
src: img.src,
position: index,
alt: img.color
}
alt = if img.color not in [nil, ""], do: img.color, else: img.name
%{src: img.src, position: index, alt: alt}
end)
end
# Find the artwork (design file) URL from the first variant's "default" file
defp extract_artwork_url(sync_variants) do
sync_variants
|> Enum.find_value(fn sv ->
(sv["files"] || [])
|> Enum.find_value(fn
%{"type" => "default", "url" => url} when is_binary(url) -> url
_ -> nil
end)
end)
end
@@ -423,10 +434,10 @@ defmodule SimpleshopTheme.Providers.Printful do
cond do
has_keyword?(name_lower, ~w[t-shirt tshirt shirt hoodie sweatshirt jogger]) -> "Apparel"
has_keyword?(name_lower, ~w[bag tote hat cap sleeve phone case]) -> "Accessories"
has_keyword?(name_lower, ~w[mug cup blanket pillow cushion throw]) -> "Homewares"
has_keyword?(name_lower, ~w[canvas poster print frame]) -> "Canvas Prints"
has_keyword?(name_lower, ~w[mug cup blanket pillow cushion]) -> "Homewares"
has_keyword?(name_lower, ~w[notebook journal]) -> "Stationery"
has_keyword?(name_lower, ~w[phone case bag tote hat cap]) -> "Accessories"
true -> "Apparel"
end
end