# `PhoenixKitEcommerce.Catalogue.ValueResolver`
[🔗](https://github.com/BeamLabEU/phoenix_kit_ecommerce/blob/0.5.11/lib/phoenix_kit_ecommerce/catalogue/value_resolver.ex#L1)

Resolves a raw label — text as it arrives from an external source such
as a Shopify option value ("Small", "Rouge") — to the value SLUG a
catalogue attribute set already uses for that same choice, creating a
new `draft` value when the set has none matching.

Built for Block 7 (the Shopify sync writing catalogue attribute-set
selections instead of raw strings); nothing in this fork calls it yet.

## Lookup order

1. Slug match: `PhoenixKit.Utils.Slug.slugify(raw_label)` against every
   value's stored `:slug` in the set (draft values included — only
   `archived` ones are excluded by `AttributeSets.list_values/2`, so a
   value this resolver created as `draft` on a previous call is found
   again rather than duplicated).
2. Exact-label match: the same whitespace-collapsed label against
   each value's `:title`.
3. Miss: a new value is created.

## Why `Slug.slugify/1` computes the slug handed to `create_value/3`

`AttributeSets.create_value/3` derives its own slug from the label only
when no explicit `:slug` is given, via a hand-rolled ASCII-only
fallback in that module with no transliteration — a non-Latin label
there slugifies to `""` and gets a random suffix instead of a stable,
readable slug. Passing the already-transliterated
`PhoenixKit.Utils.Slug.slugify/1` result as the explicit `:slug`
sidesteps that, AND keeps the value locatable by the same slug next
time this resolver runs against the same label — a random suffix would
not be reproducible across calls.

## Why the new value is created `draft`, and how

`create_value/3` has no `:status` option — every value it creates is
hardcoded `"published"`. `EntityData.bulk_update_status/3` is the only
bulk alternative, and it skips per-record changeset validation and the
per-record activity log — the wrong trade for one new row. The
shortest correct path read from the API is: create (published), then
`PhoenixKitEntities.EntityData.update/3` with `%{status: "draft"}` —
`:status` is a cast-and-`validate_inclusion/3`-checked field on
`EntityData.changeset/2`, so this is a normal, validated write, not a
raw SQL patch. (There is no `PhoenixKitEntities.update_entity_data/2`
— that name does not exist on the top-level module; the real API is
the `EntityData.update/3` used here.) Both writes run inside one
`PhoenixKit.RepoHelper.repo().transaction/1` — a failed status update
rolls the just-created (published) value back too, rather than
leaving a published, un-approved value behind with the caller seeing
only an error tuple.

Storefront facets and pickers already exclude non-`published` values
(`Query.attribute_set_counts/2` joins on `entity_data.status ==
"published"`), so a `draft` value this resolver creates does not
appear on the storefront until an admin publishes it.

## Set lookup never creates

Sets are admin-managed blueprints (`AttributeSets.create_set/2`, a
deliberate admin action) — `set_slug` must resolve to an EXISTING set
or this returns `{:error, :set_not_found}`; nothing here ever creates
a set.

# `result`

```elixir
@type result() ::
  {:ok, String.t()} | {:created, String.t()} | {:error, :set_not_found | term()}
```

# `resolve`

```elixir
@spec resolve(String.t(), String.t(), keyword()) :: result()
```

Resolves `raw_label` to a value slug within the set named `set_slug`
(bare or `"catalogue_set_"`-prefixed — the same lookup
`Query.filter_by_metadata/2` uses). `opts` is forwarded to
`AttributeSets.list_values/2` and `create_value/3` (e.g. `:actor_uuid`,
`:mode` for the activity log).

Returns `{:ok, slug}` for an existing match, `{:created, slug}` when a
new `draft` value had to be made, `{:error, :set_not_found}` when
`set_slug` doesn't resolve to a set (or whatever error `create_value/3`
/ the follow-up status update returns, on the rare write failure).

# `resolve_many`

```elixir
@spec resolve_many(String.t(), [String.t()], keyword()) :: %{
  required(String.t()) =&gt; result()
}
```

Same as `resolve/3`, but for MULTIPLE labels against the SAME set —
`resolve/3` in a loop runs `AttributeSets.list_sets/0` (a full
entities read, filtered in Elixir) and `list_values/2` once PER
LABEL, an N+1 by construction for the exact caller this module was
built for (Block 7's Shopify sync, one label per variant option per
product). This resolves the set and reads its values once and reuses
both across every label — a `:created` value from an earlier label in
the same call is visible to a later label in the same call (a value
read once at the top would go stale the moment ANY label in the batch
creates one), so two labels needing the same NEW value in one call
create it only once.

Returns `%{raw_label => resolve/3's return}`, one entry per DISTINCT
raw label (duplicates in `raw_labels` collapse to a single lookup).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
