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

Behaviour for the storefront's product/category read path.

Two adapters implement it: `PhoenixKitEcommerce.ProductSource.Legacy`
(today's `phoenix_kit_shop_products`/`phoenix_kit_shop_categories`
tables, unchanged) and, once `phoenix_kit_catalogue` is present,
`PhoenixKitEcommerce.ProductSource.Catalogue` (reads catalogue items
and returns hand-built `%Product{}`/`%Category{}` view-structs so the
facade, guards, `CartItem`, `Options` and `SeoHelpers` need no changes).

`current/0` picks the adapter at runtime; `PhoenixKitEcommerce`'s
public read functions delegate to it so callers never choose an
adapter themselves.

# `aggregate_filter_values`

```elixir
@callback aggregate_filter_values(keyword()) :: map()
```

# `get_category`

```elixir
@callback get_category(String.t(), keyword()) :: PhoenixKitEcommerce.Category.t() | nil
```

# `get_category_by_any_slug`

```elixir
@callback get_category_by_any_slug(String.t(), keyword()) ::
  {:ok, PhoenixKitEcommerce.Category.t(), String.t()} | {:error, :not_found}
```

# `get_category_by_slug_localized`

```elixir
@callback get_category_by_slug_localized(String.t(), String.t(), keyword()) ::
  {:ok, PhoenixKitEcommerce.Category.t()} | {:error, :not_found}
```

# `get_price_range_for`

```elixir
@callback get_price_range_for(keyword()) :: {Decimal.t() | nil, Decimal.t() | nil}
```

# `get_product`

```elixir
@callback get_product(String.t(), keyword()) :: PhoenixKitEcommerce.Product.t() | nil
```

# `get_product_by_any_slug`

```elixir
@callback get_product_by_any_slug(String.t(), keyword()) ::
  {:ok, PhoenixKitEcommerce.Product.t(), String.t()} | {:error, :not_found}
```

# `get_product_by_slug_localized`

```elixir
@callback get_product_by_slug_localized(String.t(), String.t(), keyword()) ::
  {:ok, PhoenixKitEcommerce.Product.t()} | {:error, :not_found}
```

# `list_categories`

```elixir
@callback list_categories(keyword()) :: [PhoenixKitEcommerce.Category.t()]
```

# `list_products`

```elixir
@callback list_products(keyword()) :: [PhoenixKitEcommerce.Product.t()]
```

# `list_products_by_ids`

```elixir
@callback list_products_by_ids([String.t()]) :: [PhoenixKitEcommerce.Product.t()]
```

# `list_products_with_count`

```elixir
@callback list_products_with_count(keyword()) ::
  {[PhoenixKitEcommerce.Product.t()], non_neg_integer()}
```

# `product_counts_by_category`

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

# `current`

Returns the adapter module for the currently active product source.

`Catalogue` only when `phoenix_kit_catalogue` is loaded AND the
`shop_product_source` config key (`phoenix_kit_shop_config`) is
`"catalogue"`; `Legacy` otherwise — including when the key is absent
or the catalogue module isn't loaded, so a host without the optional
`phoenix_kit_catalogue` dependency always gets `Legacy` regardless of
the stored key.

Reads the config on every call rather than caching it here:
`PhoenixKitEcommerce.get_config/1` is a plain primary-key
`repo().get/2` against `phoenix_kit_shop_config` (no ETS/settings-cache
layer sits in front of it — the key lives in the shop config table,
not in `PhoenixKit.Settings`, and every writer, the test suite
included, updates that row directly), so this is one point read per
facade call. Accepted so that the switch takes effect without a
restart: a per-process memo would go stale in a long-lived LiveView
process, and a node-wide cache has no invalidation hook because the
row is written without going through this module. The `Code.
ensure_loaded?/1` guard runs FIRST so a host without the optional
`phoenix_kit_catalogue` dependency never pays for the read at all.

---

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