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:
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user