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
@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".
@spec diff([PhoenixKitEcommerce.Product.t()], [map()], String.t(), keyword()) :: [ PhoenixKitEcommerce.Shopify.ProductDiff.Change.t() ]
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.
@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.