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

Ecto queries over `phoenix_kit_catalogue`'s `Item`/`Category` schemas,
scoped to the one catalogue the shop reads (`get_config("shop_catalogue")`,
default name `"decor3dprint"`, resolved through `Catalogue.list_catalogues/0`).

Returns raw catalogue structs — `PhoenixKitEcommerce.ProductSource.Catalogue`
turns them into view-structs via `Catalogue.View`. Nothing here is called
unless `ProductSource.current/0` already picked this adapter, which
requires `phoenix_kit_catalogue` to be loaded; `@compile
{:no_warn_undefined, ...}` only quietens the compiler's static xref
check for hosts that don't declare the optional dependency.

# `attribute_set_counts`

```elixir
@spec attribute_set_counts(String.t(), keyword()) :: [
  %{slug: String.t(), label: String.t(), count: non_neg_integer()}
]
```

Facet counts for one attribute SET's values (`set_slug` — the set's
entities blueprint name, with or without the `"catalogue_set_"`
prefix, same lookup `filter_by_metadata/2` uses), scoped to
storefront-visible items (`active_visibility/1`) in the shop
catalogue.

Options: `:category_uuid` (scope to one category), `:exclude_hidden_categories`
(drop items whose category's `shop_status` is `"hidden"`), `:language`
(prefer `data[language]["_title"]` over the value's bare `title` —
the picker/sidebar's fuller per-language resolution is Block 5's
remaining work; this covers the plain value label), `:catalogue_uuid`
and `:sets` (pre-resolved by the caller — see `catalogue_uuid/0` and
`list_sets/0`).

A value with no `published` `EntityData` row for the requested slug
never appears — `draft`/`archived` values (Block 5's resolver creates
unknown Shopify strings as `draft`) are excluded from storefront
facets by construction, not by a separate filter.

Ordered by the value's position, then its resolved label.

# `catalogue_uuid`

```elixir
@spec catalogue_uuid() :: Ecto.UUID.t() | nil
```

The uuid of the one catalogue the shop reads, or `nil` when it can't
be resolved (not yet bootstrapped, or the configured name matches
none). Resolved by name — a config read plus `Catalogue.list_catalogues/0`
— so every public function here accepts a pre-resolved value as
`opts[:catalogue_uuid]` (`nil` included, meaning "resolved, none")
and only falls back to this when the caller passed nothing;
`ProductSource.Catalogue` resolves it once per facade call and
threads it through, so a listing page pays for this once, not once
per query it composes.

# `category_item_image_options`

```elixir
@spec category_item_image_options(Ecto.UUID.t() | nil) :: [
  %{name: String.t(), uuid: Ecto.UUID.t(), image_uuid: Ecto.UUID.t()}
]
```

Items belonging to one category that carry an image (own
`featured_image_uuid` or a non-empty `media_order`), `{name, uuid}`
pairs ordered by position then name — the same candidates
`resolve_category_images/1`'s auto-detect step would pick the first
of. Backs the category form's featured-item picker (`ShopSections.
category/1`), so an admin only ever sees items eligible to actually
supply the category's fallback image. Soft-deleted items are excluded;
otherwise unfiltered by shop status — an explicit pick is allowed to
name a draft item, same as `featured_item_uuid` always could as a raw
uuid.

# `get_category`

```elixir
@spec get_category(String.t(), keyword()) ::
  PhoenixKitCatalogue.Schemas.Category.t() | nil
```

Fetches one category by uuid, scoped to the shop catalogue.

# `get_item`

```elixir
@spec get_item(String.t(), keyword()) :: PhoenixKitCatalogue.Schemas.Item.t() | nil
```

Fetches one item by uuid, scoped to the shop catalogue. `nil` on a miss.

# `list_categories`

```elixir
@spec list_categories(keyword()) :: [PhoenixKitCatalogue.Schemas.Category.t()]
```

Lists the shop catalogue's categories. Options: `:status`/`:parent_uuid`
(see `apply_category_filters/2`), `:search` (the admin category search
box — matches the primary `name` and every language bucket's `_name`),
`:catalogue_uuid` (pre-resolved).

# `list_categories_by_uuids`

```elixir
@spec list_categories_by_uuids([String.t()], keyword()) :: [
  PhoenixKitCatalogue.Schemas.Category.t()
]
```

Fetches categories by uuid, scoped to the shop catalogue. Missing uuids dropped.

# `list_items`

```elixir
@spec list_items(keyword()) :: [PhoenixKitCatalogue.Schemas.Item.t()]
```

Lists items matching `opts`, ordered by `position, name`.

# `list_items_by_uuids`

Fetches items by uuid, order preserved, missing uuids dropped, scoped
to the shop catalogue (same "one catalogue only" contract every other
read in this module enforces) — mirrors
`PhoenixKitEcommerce.ProductSource.Legacy.list_products_by_ids/1`.

# `list_items_with_count`

```elixir
@spec list_items_with_count(keyword()) ::
  {[PhoenixKitCatalogue.Schemas.Item.t()], non_neg_integer()}
```

`list_items/1` plus the total count before pagination is applied.

# `price_range`

```elixir
@spec price_range(keyword()) :: {Decimal.t() | nil, Decimal.t() | nil}
```

Min/max `base_price` over active items, optionally scoped to a category.

# `product_counts_by_category`

```elixir
@spec product_counts_by_category(keyword()) :: %{
  required(String.t()) =&gt; non_neg_integer()
}
```

Active-item counts grouped by `category_uuid`, "active" meaning
`item.status == "active"` and `COALESCE(shop_status, 'active') =
'active'` (spec principle 7, same fallback as the listing) — items
with no category are excluded, same as
`ProductSource.Legacy.product_counts_by_category/0`.

# `resolve_category_images`

```elixir
@spec resolve_category_images([PhoenixKitCatalogue.Schemas.Category.t()], keyword()) ::
  %{
    required(Ecto.UUID.t()) =&gt; Ecto.UUID.t()
  }
```

Resolves `View.category_view/2`'s priority-2 image fallback for a batch
of categories, `category_uuid => image uuid`, in AT MOST two item
queries plus the one catalogue lookup they share — never one per
category (`ProductSource.Catalogue.list_categories/1` builds a view for
every category in one pass, and the storefront/admin category lists have
no pagination ceiling on that count). The cost is fixed: it does not
grow with how many categories are passed in.

For each category, the source is:
1. An explicit `data["ecommerce"]["featured_item_uuid"]` — that item's
   own `data["featured_image_uuid"]`, falling back to the first entry
   of its `data["media_order"]`. Resolved for every category that set
   one, in a single `list_items_by_uuids/1` call.
2. Otherwise, auto-detect: the first `active_visibility/1` item in the
   category (ordered by `position`, then `name`) that carries an image
   by the same rule — exactly the old admin form's "Auto-detect (first
   product with image)" hint. Resolved for every OTHER category in one
   query fetching all their active items once, then walking the
   (already category/position-ordered) rows in Elixir to keep the
   first image-bearing one per category.

A category absent from the result has no image from either step (its
`image_uuid`, if any — priority 1 — is a category-view concern, not
this function's).

# `set_display_names`

```elixir
@spec set_display_names([Ecto.UUID.t()], String.t()) :: %{
  required(Ecto.UUID.t()) =&gt; String.t()
}
```

Translated display names for a batch of attribute-set BLUEPRINTS,
keyed by set uuid (`AttributeSets.resolve_for_item/2`'s per-set `:uuid`
field) — `ProductSource.Catalogue` swaps a resolved set's `:name` for
this before handing `sets` to `View.product_view/2` (which is pure and
has no way to read `settings["translations"]` itself). One
`AttributeSets.get_set/2` call per DISTINCT set, never per item — a
product page has a handful of attached sets, and a listing page's many
items still share that same handful, so the count stays small; there
is no batched-by-uuid-list entities read to reach for instead. A set
that can't be resolved (deleted, or gated by `get_set/2`'s own owner
check) is simply absent from the result — callers keep the
untranslated `:name` already on the resolved set for it.

# `set_label`

```elixir
@spec set_label(String.t(), String.t()) :: String.t() | nil
```

Translated display name of one attribute set by its FILTER-CONFIG slug
(`set_slug` — with or without the `catalogue_set_` prefix, same lookup
`filter_by_metadata/2` uses), `nil` when the slug doesn't resolve to a
set. The sidebar's `attribute_set`/`metadata_option` filter section
reads this — a filter config only ever carries the slug, never the
set's uuid.

# `vendor_counts`

```elixir
@spec vendor_counts(keyword()) :: [%{value: String.t(), count: non_neg_integer()}]
```

Distinct `vendor` values (from `data["ecommerce"]["vendor"]`) over
active items, with counts, optionally scoped to a category.

---

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