# `PhoenixKitCatalogue.Catalogue.ItemSupplierInfos`
[🔗](https://github.com/BeamLabEU/phoenix_kit_catalogue/blob/0.12.3/lib/phoenix_kit_catalogue/catalogue/item_supplier_infos.ex#L1)

Context for managing the `phoenix_kit_cat_item_supplier_info` junction table.

Each row links a catalogue item to a supplier (local or CRM-sourced) and
carries a snapshot of the supplier's name at write time so the record stays
readable even after renaming or deletion.

At most one row per item may have `is_primary: true` — enforced by a partial
unique index and by `set_primary/1`, which uses a transaction to clear any
existing primary before promoting the target row.

## Price history via validity windows

The schema carries `valid_from`/`valid_to` date fields. A row is considered
*current* when `valid_to` is `nil`. `revise_unit_cost/3` implements a
non-destructive price revision: the current row is closed (`valid_to: today`,
`is_primary: false`) and a successor row is inserted with the new cost,
`valid_from: today`, and `valid_to: nil`. This produces an append-only price
history ordered by `valid_from` descending. The canonical "current" predicate
is `is_nil(valid_to)`.

# `create`

```elixir
@spec create(
  map(),
  keyword()
) ::
  {:ok, PhoenixKitCatalogue.Schemas.ItemSupplierInfo.t()}
  | {:error, Ecto.Changeset.t(PhoenixKitCatalogue.Schemas.ItemSupplierInfo.t())}
```

Creates a supplier-info row.

When the item has no primary supplier yet, the newly linked row is
auto-promoted to primary (an item with suppliers but no primary is a
valid state only when the user explicitly demotes it).

# `delete`

```elixir
@spec delete(
  PhoenixKitCatalogue.Schemas.ItemSupplierInfo.t(),
  keyword()
) ::
  {:ok, PhoenixKitCatalogue.Schemas.ItemSupplierInfo.t()}
  | {:error, Ecto.Changeset.t(PhoenixKitCatalogue.Schemas.ItemSupplierInfo.t())}
```

Deletes a supplier-info row.

# `get`

```elixir
@spec get(Ecto.UUID.t()) :: PhoenixKitCatalogue.Schemas.ItemSupplierInfo.t() | nil
```

Fetches a supplier-info row by UUID. Returns `nil` if not found.

# `history_for_pair`

```elixir
@spec history_for_pair(Ecto.UUID.t(), Ecto.UUID.t()) :: [
  PhoenixKitCatalogue.Schemas.ItemSupplierInfo.t()
]
```

Returns all rows for an item/supplier pair ordered newest-first.

Includes both current (`valid_to: nil`) and closed rows so callers can
display a full price revision history. Ordered by `valid_from` descending
(nulls last), then `inserted_at` descending.

# `list_for_item`

```elixir
@spec list_for_item(Ecto.UUID.t()) :: [
  PhoenixKitCatalogue.Schemas.ItemSupplierInfo.t()
]
```

Lists *current* supplier-info rows for an item, ordered by position then
inserted_at.

A row is current when `valid_to` is `nil`. Closed rows (produced by
`revise_unit_cost/3`) are excluded; use `history_for_pair/2` to retrieve the
full history for a specific item/supplier pair.

# `primary_for_item`

```elixir
@spec primary_for_item(Ecto.UUID.t()) ::
  PhoenixKitCatalogue.Schemas.ItemSupplierInfo.t() | nil
```

Returns the *current* primary supplier-info row for an item, or `nil` if
none is marked primary.

Only current rows (`valid_to: nil`) are considered — closed revision rows
carry `is_primary: false` by construction and are excluded.

# `revise_unit_cost`

```elixir
@spec revise_unit_cost(
  PhoenixKitCatalogue.Schemas.ItemSupplierInfo.t(),
  Decimal.t(),
  keyword()
) ::
  {:ok, PhoenixKitCatalogue.Schemas.ItemSupplierInfo.t()}
  | {:error, :not_current | Ecto.Changeset.t()}
```

Closes the current junction row and inserts a successor with the new unit
cost, creating an append-only price revision history.

## Guards

  * `{:error, :not_current}` — `info.valid_to` is not `nil`; only the current
    row can be revised.
  * `{:ok, info}` (no-op) — `new_cost` equals `info.unit_cost` (or both
    effectively zero when `unit_cost` is `nil`), *and* `opts[:currency]` is
    either absent or matches the row's current currency. A currency-only
    change (same cost, different `:currency`) still creates a revision.

## Transaction

Within a single Ecto.Multi transaction:

1. The current row is closed: `valid_to` is set to today and `is_primary` is
   forced to `false` via `force_change/3`, freeing the partial-unique index
   inside the tx so the successor can inherit the primary flag.
2. A successor row is inserted copying `item_uuid`, `supplier_uuid`,
   `supplier_source`, `supplier_name_snapshot`, `supplier_sku`, `currency`,
   `lead_time_days`, `min_order_qty`, `position`, and `metadata` from the
   closed row. `unit_cost` is set to `new_cost`; `valid_from` is today;
   `valid_to` is `nil`; `is_primary` inherits the original row's value.

## Options

  * `:actor_uuid` — UUID of the user performing the revision (persisted in the
    activity log).
  * `:source` — free-form source label (e.g. `"goods_receipt"`).
  * `:source_uuid` — UUID of the originating resource (e.g. receipt UUID).
  * `:currency` — when provided and different from the row's currency, the
    successor stores the new currency; both old and new currencies are
    recorded in the activity metadata.

## Currency awareness

The successor always keeps the row's own currency unless `opts[:currency]` is
given and differs, in which case the new currency is stored and the activity
log captures both.

# `set_primary`

```elixir
@spec set_primary(
  PhoenixKitCatalogue.Schemas.ItemSupplierInfo.t(),
  keyword()
) :: {:ok, PhoenixKitCatalogue.Schemas.ItemSupplierInfo.t()} | {:error, any()}
```

Promotes a supplier-info row to primary for its item.

Runs in a transaction: clears `is_primary` on all sibling rows first,
then sets `is_primary: true` on the target. Respects the partial unique
index — concurrent callers produce a constraint violation rather than
double-marking.

# `update`

```elixir
@spec update(PhoenixKitCatalogue.Schemas.ItemSupplierInfo.t(), map(), keyword()) ::
  {:ok, PhoenixKitCatalogue.Schemas.ItemSupplierInfo.t()}
  | {:error, Ecto.Changeset.t(PhoenixKitCatalogue.Schemas.ItemSupplierInfo.t())}
```

Updates a supplier-info row.

---

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