feat: add admin provider setup UI with improved product sync

- Add /admin/providers LiveView for connecting and managing POD providers
- Implement pagination for Printify API (handles all products, not just first page)
- Add parallel processing (5 concurrent) for faster product sync
- Add slug-based fallback matching when provider_product_id changes
- Add error recovery with try/rescue to prevent stuck sync status
- Add checksum-based change detection to skip unchanged products
- Add upsert tests covering race conditions and slug matching
- Add Printify provider tests
- Document Printify integration research (product identity, order risks,
  open source vs managed hosting implications)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-01-31 22:08:34 +00:00
parent bbd748f123
commit 5b736b99fd
17 changed files with 1352 additions and 38 deletions

View File

@@ -59,19 +59,34 @@ defmodule SimpleshopTheme.Sync.ProductSyncWorker do
# Private
# =============================================================================
# Number of concurrent product syncs (DB operations only, not API calls)
@max_concurrency 5
defp sync_products(conn) do
Logger.info("Starting product sync for #{conn.provider_type} (#{conn.id})")
Products.update_sync_status(conn, "syncing")
try do
do_sync_products(conn)
rescue
e ->
Logger.error("Product sync crashed for #{conn.provider_type}: #{Exception.message(e)}")
Products.update_sync_status(conn, "failed")
{:error, :sync_crashed}
end
end
defp do_sync_products(conn) do
with {:ok, provider} <- Provider.for_connection(conn),
{:ok, products} <- provider.fetch_products(conn) do
Logger.info("Fetched #{length(products)} products from #{conn.provider_type}")
results = sync_all_products(conn, products)
created = Enum.count(results, fn {_, _, status} -> status == :created end)
updated = Enum.count(results, fn {_, _, status} -> status == :updated end)
unchanged = Enum.count(results, fn {_, _, status} -> status == :unchanged end)
errors = Enum.count(results, fn result -> match?({:error, _}, result) end)
created = Enum.count(results, &match?({:ok, _, :created}, &1))
updated = Enum.count(results, &match?({:ok, _, :updated}, &1))
unchanged = Enum.count(results, &match?({:ok, _, :unchanged}, &1))
errors = Enum.count(results, &match?({:error, _}, &1))
Logger.info(
"Product sync complete for #{conn.provider_type}: " <>
@@ -89,18 +104,35 @@ defmodule SimpleshopTheme.Sync.ProductSyncWorker do
end
defp sync_all_products(conn, products) do
Enum.map(products, fn product_data ->
case sync_product(conn, product_data) do
{:ok, product, status} ->
sync_product_associations(product, product_data)
{:ok, product, status}
error ->
error
end
products
|> Task.async_stream(
fn product_data -> sync_single_product(conn, product_data) end,
max_concurrency: @max_concurrency,
timeout: 30_000,
on_timeout: :kill_task
)
|> Enum.map(fn
{:ok, result} -> result
{:exit, :timeout} -> {:error, :timeout}
{:exit, reason} -> {:error, reason}
end)
end
defp sync_single_product(conn, product_data) do
case sync_product(conn, product_data) do
{:ok, product, status} ->
sync_product_associations(product, product_data)
{:ok, product, status}
error ->
Logger.warning(
"Failed to sync product #{product_data[:provider_product_id]}: #{inspect(error)}"
)
error
end
end
defp sync_product(conn, product_data) do
attrs = %{
provider_product_id: product_data[:provider_product_id],