Picks where a sync reads Shopify product data from: the Admin API
(primary, full-fidelity) or the public storefront JSON endpoint
(fallback, price-only — see PhoenixKitEcommerce.Shopify.StorefrontClient
for why it is deliberately narrow).
Split into a pure decision core (decide/2) and a thin I/O shell
(fetch/2). That split is not, on its own, a testability necessity in
this repo — test/test_helper.exs excludes DataCase tests only when
no test database is reachable, and one normally is (createdb phoenix_kit_ecommerce_test; see that file for details). It earns its
place anyway: the fallback/abort decision — which is the one property
worth being certain about, since getting it wrong either silently
narrows a sync to price-only or leaves it stuck on a dead token — is
fully expressible without a domain lookup, an HTTP call, or a database,
so keeping it in a pure function makes every branch a plain data-in,
data-out assertion instead of an HTTP-stub scenario.
Falling back to the storefront is only correct when the Admin API failure is about the credential — a token that's missing, wrong, or no longer authorized. Anything else (a rate limit, a 5xx, a timeout) is a transient problem with the Admin API itself; falling back on those would silently narrow the sync to price-only and report "no text changes" when the truth is "we could not check". Those abort instead.
Summary
Types
Functions
@spec decide({:ok, [map()]} | {:error, term()}, {:ok, String.t()} | {:error, term()}) :: {:use_admin, [map()], [atom()]} | {:use_storefront, String.t(), term(), [atom()]} | {:abort, term()}
Decides how to source products, given the Admin API's fetch result and an (already looked-up) shop domain, and what field set the resulting products carry.
Pure — no I/O. Rules:
- Admin succeeded →
{:use_admin, products, only},onlybeingProductDiff.comparable_fields/0— the Admin API carries every field. - Admin failed with a credential error (
:unauthorized,:missing_credentials,:forbidden) and a shop domain is available →{:use_storefront, domain, reason, only}—reasonis the Admin failure that triggered the fallback,onlyis[:price]. - Admin failed with a credential error but no shop domain is
available →
{:abort, reason}— there is nothing to fall back to. - Admin failed with anything else (rate limited, 5xx, timeout, ...)
→
{:abort, reason}, regardless of whether a domain is available.
Fetches Shopify products for integration_uuid, preferring the Admin
API and falling back to the public storefront per decide/2.
result.only travels with the result so a caller doesn't have to
re-derive it: ProductDiff.comparable_fields/0 for the Admin source,
[:price] for the storefront source — hand it straight to
ProductDiff.diff/4's :only option.
The shop domain is only looked up when the Admin API actually failed —
decide/2 never needs it on the success path (see its first clause),
so this is meant to spare a successful sync a second
PhoenixKit.Integrations.get_credentials/1 call it would otherwise
make and discard. That is the intent, not a pinned guarantee: nothing
in this suite currently asserts the call count.
Options
:admin_options— forwarded toAdminClient.fetch_products/2.:storefront_options— forwarded toStorefrontClient.fetch_products/2.