Pure mapping from a Shopify Admin API product payload's "options"/
"variants" into the shape PhoenixKitEcommerce.Catalogue.Writer. sync_variants/2 attaches as catalogue attribute sets: one set per real
option (Shopify's auto-generated single-option "no variants" product —
option name "Title", its lone value "Default Title" — is skipped,
same as it never becomes a _option_slots entry on the legacy CSV
path), values in the order they first appear across variants[], and
a per-value price modifier.
No I/O, no catalogue/entities lookups — build/1 never needs a live
set to exist. slug (both the set's and each modifier map's key) is
PhoenixKitEcommerce.Catalogue.SetSlug.normalise/1 of the option's
OWN name; resolving a raw variant-option VALUE label to a value slug
is a separate, stateful step (PhoenixKitEcommerce.Catalogue. ValueResolver) Writer.sync_variants/2 runs afterward, against
whatever catalogue set that slug now names.
Modifier rule
For each option, group variants[] by that option's value
(variant["option<position>"]); a value's modifier is
min(price of variants carrying that value) − min(price of every priced variant on the product). Grouping by MIN rather than "first
variant seen" (the legacy Import.OptionBuilder's rule, correct only
when a single option drives price) matters here because two options
combine on one variant — the cheapest variant carrying value X is the
fair anchor for X's modifier regardless of what its OTHER option
happened to be. The overall cheapest value's modifier is exactly
Decimal.new("0.00") (equal minima, not a rounded near-zero) as long
as Shopify's own price strings carry two decimals, which
Decimal.sub/2 preserves.
Non-additive matrices are reported, not hidden
Per-option modifiers can only ever express an ADDITIVE price matrix:
the storefront prices a selection as base + Σ modifier(value). A
Shopify matrix that is not additive — S/L × Red/Blue at 10/12/15/20
gives S:0, L:5, Red:0, Blue:2 and predicts 17 for L-Blue, where
Shopify charges 20 — is silently under-priced by that reconstruction.
build/1 therefore re-derives every priced variant's price from the
modifiers it just computed and lists each mismatch in :warnings
(one human-readable line per variant, naming the product), logging
each at :warning as well. The modifiers are still returned — they
are the best additive fit — but a caller writing them must surface the
warnings (the media sync worker records them on the run's per-product
errors) rather than let the write pass as clean.
Summary
Functions
Builds %{sets: [...], modifiers: %{set_slug => %{label => Decimal}}, warnings: [...]} from shopify_product's "options" and
"variants". Both keys default to [] when absent (a payload with no
options at all — every variant on the default "Title" option — yields
sets: [], modifiers: %{}, warnings: []). See the moduledoc for what
:warnings carries.
Types
Functions
Builds %{sets: [...], modifiers: %{set_slug => %{label => Decimal}}, warnings: [...]} from shopify_product's "options" and
"variants". Both keys default to [] when absent (a payload with no
options at all — every variant on the default "Title" option — yields
sets: [], modifiers: %{}, warnings: []). See the moduledoc for what
:warnings carries.