# `PhoenixKitEcommerce.ProductSource.Catalogue.View`
[🔗](https://github.com/BeamLabEU/phoenix_kit_ecommerce/blob/0.5.11/lib/phoenix_kit_ecommerce/product_source/catalogue/view.ex#L1)

Builds hand-built `%PhoenixKitEcommerce.Product{}` / `%PhoenixKitEcommerce.Category{}`
view-structs from a `phoenix_kit_catalogue` item/category, so the facade,
`Options`, `PriceDisplay`, `CartItem` and the storefront templates read
exactly the field names they read today from `phoenix_kit_shop_products`/
`phoenix_kit_shop_categories` — no behavior change on their side.

Every function here is pure: no `Repo` call, no write. `product_view/2`
and `category_view/2` build the struct with `struct(Product|Category,
fields)` — `__meta__` stays `:built`, which is exactly what
`PhoenixKitEcommerce.update_product/2`/`delete_product/2` refuse.

Accepts duck-typed records (a real `%PhoenixKitCatalogue.Schemas.Item{}`/
`%PhoenixKitCatalogue.Schemas.Category{}`, or a plain map with the same
keys) — nothing here pattern-matches on the catalogue structs, so tests
can feed plain maps without the optional `phoenix_kit_catalogue`
dependency loaded.

# `category_view`

```elixir
@spec category_view(map(), keyword()) :: PhoenixKitEcommerce.Category.t()
```

Builds a `%Category{}` view-struct from a catalogue category.

`opts[:parent]` sets the `:parent` association field (a `%Category{}`
view-struct, or `nil` for a root category / when the caller didn't ask
for it) — a view-struct can never be `Repo.preload/2`'d, so leaving the
default `Ecto.Association.NotLoaded` in place (as every OTHER unset
`belongs_to`/`has_many` field on this struct still does) would crash
the first template that does a plain `if category.parent do` truthy
check, same as `PhoenixKitEcommerce.ProductSource.Catalogue`'s
`single_category/2` already resolves `:category` for products.

`:storefront_filters` is read straight off
`data["ecommerce"]["storefront_filters"]` (`%{}` when absent) — see
`PhoenixKitEcommerce.merge_storefront_filters/2` for how it overrides
the global filter config.

`opts[:featured_image_uuid]` is the image uuid `ProductSource.Catalogue.
resolve_category_images/1`'s (a `Query.resolve_category_images/1` call)
already resolved for THIS category — this module stays pure, so it
cannot look up the featured item's own image itself. When given, it's
wrapped as a `%Product{featured_image_uuid: ...}` and attached under
`:featured_product`, which is exactly the shape `Category.
get_image_url/2`'s priority-2 step (written for the legacy `Repo.
preload(:featured_product)` source) already reads — no change needed
there. `nil` (every call site before this option existed, and any
category with no resolved image) leaves `:featured_product` `nil`,
which that same priority-2/3 pattern match simply skips.

# `legacy_metadata`

```elixir
@spec legacy_metadata(map(), list() | map(), String.t() | nil) :: map()
```

Synthesizes the legacy `metadata` sub-map every option/price-display
reader (`Options`, variant picker, `CartItem.from_product/3`) expects:
`_option_values` (labels in the item's stored selection order),
`_price_modifiers` (slug-keyed `ecommerce.price_modifiers` swapped to
the label keys those readers match on), `_option_labels` (`%{set_slug
=> set display name}`, read straight off `sets` — see the moduledoc
note on `product_view/2`'s `:language`, since a set's OWN translated
name has to already be sitting on it by the time it gets here) and
`_value_slugs` (`%{set_slug => %{label => value slug}}`, every value in
the set, not only the ones selected — Block 6/7's future slug-based
`selected_specs` reads this; nothing in THIS block writes it back).
All four computed fresh from `sets` on every call, merged over whatever
else `data["ecommerce"]["legacy_metadata"]` snapshotted (`_option_slots`,
`_image_mappings`, `_price_display`, …) — minus those same four keys,
so a stale snapshot never shadows the live attachment state.

`sets` accepts the same shapes `product_view/2`'s `:sets` option does.
`language` (default `nil`) picks which translation of each VALUE's
label `_option_values`/`_price_modifiers`/`_value_slugs` key on —
`nil` keeps every one of them exactly as `product_view/2` built it
before this option existed. `_option_labels` doesn't take `language`
at all: it reads whatever `:name` already sits on each set in `sets`.

# `product_view`

```elixir
@spec product_view(map(), keyword()) :: PhoenixKitEcommerce.Product.t()
```

Builds a `%Product{}` view-struct from a catalogue item.

`opts[:sets]` is a `PhoenixKitCatalogue.Catalogue.AttributeSets.resolve_for_item/2`
result (`%{schema_version: _, sets: [...]}`) — or, for tests and other
callers that already have the per-item list, the bare `sets` list
itself. `opts[:category]` is a `category_view/2` result, attached as
`:category` when the caller preloaded one.

`opts[:language]` (a language code, or `nil` for the untranslated
labels `sets` already carries) picks the language `metadata`'s
`_option_values`/`_price_modifiers`/`_value_slugs` use for each
attribute-set VALUE's label — see `legacy_metadata/3`. It does NOT
translate a set's own `:name` (`_option_labels`, below) — a value's
translation lives right on it (`values[].extras`, whatever
`resolve_set/2` read off the value's `EntityData.data`), so this
pure module can pick it with a plain map lookup, but a set's
`settings["translations"]` isn't part of the resolved shape at all;
translating `:name` needs an actual read
(`ProductSource.Catalogue.Query.set_display_names/2`) that only the
caller building `sets` (`ProductSource.Catalogue`) can do before
handing them to this pure function.

`opts[:base_currency]` overrides the shop's configured base currency
code used as the fallback when the item's own `data["ecommerce"]["currency"]`
is absent — every real caller omits it (falling through to
`base_currency_code/0`, which reads `PhoenixKitEcommerce.get_base_currency/0`),
so this stays a pure function for callers (tests included) that want to
avoid that DB read entirely.

`opts[:languages]` overrides which language codes `language_keys/2` treats
as enabled (see its doc) — every real caller omits it (falling through to
`Translations.enabled_languages/0`, itself backed by the Languages
module's settings), so tests can pin a fixed set of languages without
that dependency either.

---

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