What "in scope" means for a Shopify → catalogue sync, when the connected
store carries far more products than this shop wants in its catalogue
(e.g. a store with 2750 Shopify products syncing only ~665 of them,
selected by a catalog-3d tag).
Persisted as one phoenix_kit_shop_config row under "shopify_sync_scope"
(PhoenixKitEcommerce.get_config/1/default_config_value/1, mirroring
"shopify_collections_filter"'s own key/default pattern), shape:
%{"mode" => "all" | "filtered", "tags" => [String.t()],
"product_types" => [String.t()]}"all" — the default, and what an operator who never configured this
gets — means every Shopify product is in scope; "filtered" narrows to
products carrying at least one of "tags" (when non-empty) AND whose
product_type is in "product_types" (when non-empty). A "filtered"
scope with both lists empty behaves exactly like "all" — see
in_scope?/2.
This module is deliberately generic (no hardcoded tag or product type):
a different shop binds it to a different tag, a product type, or leaves
it at "all" — see PhoenixKitEcommerce.Workers.ShopifyMediaSyncWorker
and PhoenixKitEcommerce.Shopify.Sync.check/2, the two callers that
consult it to decide what an UNMATCHED Shopify product means: in scope,
it's missing from the catalogue and worth flagging; out of scope, its
absence is by design and must never be reported as an error or offered
for import. Neither caller ever scopes a product the catalogue ALREADY
has — see each one's own moduledoc for why.
Summary
Functions
Whether scope is a "filtered" scope (as opposed to "all").
Reads the current scope from phoenix_kit_shop_config, normalized.
Whether a Shopify product (raw Admin API map) falls within scope.
Splits products into {in_scope, out_of_scope} per in_scope?/2,
preserving each side's relative order.
Same as partition/2, but for a list of ITEMS that each wrap a raw
Shopify product rather than being one — extract_fun pulls the
product map out of each item for the in_scope?/2 check, while both
output lists still carry the original items, not the extracted
products. Shopify.Sync.check/2's own :new_products uses this to
partition ProductDiff.Change structs by their .shopify_product
without a second, independent Enum.split_with/2.
Validates and persists a scope, insert-or-update on the single
"shopify_sync_scope" row (same pattern as
PhoenixKitEcommerce.update_storefront_filters/1).
Types
Functions
@spec all() :: t()
The scope that means "everything" — get/0's default.
Whether scope is a "filtered" scope (as opposed to "all").
@spec get() :: t()
Reads the current scope from phoenix_kit_shop_config, normalized.
Never raises on a missing or malformed stored value — anything that
isn't a recognizable "filtered" scope reads as all(), the same
fail-open posture CollectionSync's own filter default takes.
Whether a Shopify product (raw Admin API map) falls within scope.
mode: :all (or a :filtered scope with both lists empty — see the
moduledoc) always returns true. Otherwise: true when (tags is
empty OR the product carries at least one of them) AND
(product_types is empty OR the product's "product_type" is one of
them) — both sides must pass, so a scope configured with only tags
ignores product type entirely, and vice versa.
Shopify's "tags" field is a single comma-separated string on the
Admin API (occasionally already a list, e.g. from a test fixture or a
future API version); either shape is accepted. Comparison is
case-insensitive and trims whitespace on both sides, matching how an
operator is likely to have typed the scope's own tags.
Splits products into {in_scope, out_of_scope} per in_scope?/2,
preserving each side's relative order.
Same as partition/2, but for a list of ITEMS that each wrap a raw
Shopify product rather than being one — extract_fun pulls the
product map out of each item for the in_scope?/2 check, while both
output lists still carry the original items, not the extracted
products. Shopify.Sync.check/2's own :new_products uses this to
partition ProductDiff.Change structs by their .shopify_product
without a second, independent Enum.split_with/2.
@spec put(map()) :: {:ok, t()} | {:error, :invalid_mode | Ecto.Changeset.t()}
Validates and persists a scope, insert-or-update on the single
"shopify_sync_scope" row (same pattern as
PhoenixKitEcommerce.update_storefront_filters/1).
Accepts the same shape get/0 returns (string OR atom keys, string OR
atom mode), normalizes it (trims tags/product types, drops blanks,
dedupes, downcases nothing — Shopify tags/product types are compared
case-insensitively at match time in in_scope?/2, not folded here so
the stored value still reads back as the operator typed it), and
refuses anything whose mode isn't "all"/"filtered" (or :all/
:filtered) with {:error, :invalid_mode}.