PhoenixKitEcommerce.Shopify.ProductDiff (PhoenixKitEcommerce v0.4.2)

Copy Markdown View Source

Compares local products against Shopify Admin API product data.

Matching is by product.slug[base_locale] == shopify_product["handle"]. A Shopify product with no local match is skipped — this module never creates products, only flags differences on ones that already exist (product creation stays the CSV importer's job).

Compared fields: title, body_html, description, vendor, tags, status, price. title/body_html/description are localized fields; only the base locale is read/compared here (writing them back is Shopify.Sync.apply_change/2's job). diff/4's opts[:only] narrows this set to a chosen list of fields — needed because some Shopify data sources (e.g. a public storefront fallback) only ever carry a subset of fields, such as price alone. Comparing an absent field against a present local value would otherwise be reported as a deletion, and applying such a change would erase real data.

diff/4 (and its diff/2/diff/3 arities) is pure — no network or database access — so it can be tested directly with in-memory product structs and Shopify API response maps.

Summary

Functions

The full set of fields diff/4 can compare — its opts[:only] default, and what a caller (e.g. PhoenixKitEcommerce.Shopify.Source) should pass for a source that carries every field, such as the Admin API. There is no :all sentinel accepted by opts[:only] — passing this list explicitly is the correct way to ask for "everything".

Matches shopify_products to local_products by handle and returns one Change per match that has at least one real difference. Shopify products without a matching local product are skipped.

Counts distinct local_products matched by handle to a shopify_products entry — the same matching rule diff/4 uses (product.slug[base_locale] == shopify_product["handle"]), but independent of whether the match has any actual field difference.

Functions

comparable_fields()

@spec comparable_fields() :: [atom()]

The full set of fields diff/4 can compare — its opts[:only] default, and what a caller (e.g. PhoenixKitEcommerce.Shopify.Source) should pass for a source that carries every field, such as the Admin API. There is no :all sentinel accepted by opts[:only] — passing this list explicitly is the correct way to ask for "everything".

diff(local_products, shopify_products, base_locale \\ Translations.default_language(), opts \\ [])

Matches shopify_products to local_products by handle and returns one Change per match that has at least one real difference. Shopify products without a matching local product are skipped.

base_locale defaults to Translations.default_language/0, which reads through PhoenixKit.Settings — pass it explicitly to keep a call free of database access (e.g. in tests).

opts[:only] restricts the comparison to the given list of field atoms (a subset of [:title, :body_html, :description, :vendor, :tags, :status, :price]), defaulting to all of them. Pass it when the incoming Shopify data only ever carries some fields — e.g. a public-storefront fallback source that reads price alone — so the fields it doesn't carry aren't reported (and later applied) as deletions. An unknown key in opts raises, so a typo (e.g. onlyy:) can't silently fall back to comparing every field. Every element of opts[:only] itself is validated against [:title, :body_html, :description, :vendor, :tags, :status, :price] too — a typo'd field atom there (only: [:titel]) fails the opposite way a missing :only does: instead of comparing too much, it would compare nothing and report a catalog "in sync" that was never actually checked, which is the worse failure mode for a sync tool. only: [] (compare nothing, deliberately) is not an error — every element of an empty list is vacuously valid.

base_locale must be a string. This guards against the easy mistake of passing opts as the third argument and dropping base_locale entirely (diff(local, shopify, only: [:price])) — without the guard that silently matches nothing and returns [], instead of raising.

matched_count(local_products, shopify_products, base_locale \\ Translations.default_language())

@spec matched_count([PhoenixKitEcommerce.Product.t()], [map()], String.t()) ::
  non_neg_integer()

Counts distinct local_products matched by handle to a shopify_products entry — the same matching rule diff/4 uses (product.slug[base_locale] == shopify_product["handle"]), but independent of whether the match has any actual field difference.

This is "how much of the Shopify catalog a sync can even see": every local product with no matching Shopify handle is invisible to diff/4 regardless of :only (see this module's moduledoc — a Shopify product with no local match is skipped, and the reverse is equally true: a local product with no Shopify-side handle never reaches build_change/4 at all). Reuses diff/4's own index_by_handle/2, so this can never drift from what diff/4 actually matches.

base_locale defaults to Translations.default_language/0, same as diff/4 — pass it explicitly to keep a call free of that default's database access.