# `PhoenixKitWarehouse.CostProposals`
[🔗](https://github.com/BeamLabEU/phoenix_kit_warehouse/blob/0.4.0/lib/phoenix_kit_warehouse/cost_proposals.ex#L1)

Stateless derivation of purchase price proposals for posted goods receipts.

A proposal is generated when a receipt line's `unit_value` (the price paid)
differs from the current catalogued unit cost for the same item/supplier pair.

## No proposal without a junction row

When the catalogue has no `item_supplier_info` junction row for an
item/supplier pair, **no proposal is generated**. Linking a supplier to an
item is a deliberate catalogue action, not a side-effect of receiving goods.
This boundary is intentional: the warehouse must not implicitly create or
imply catalogue relationships.

## Degradation without catalogue exports

`catalogue_resolver/0` returns a resolver function that guards its call
with `Code.ensure_loaded?` + `function_exported?`. When the catalogued
`Catalogue.Suppliers.active_info_for/2` is absent (older release), the
resolver returns `nil` for all pairs and `derive/3` yields no proposals.
No crash, no error — the feature silently degrades to a no-op.

# `apply_revision`

```elixir
@spec apply_revision(
  map(),
  keyword()
) :: {:ok, any()} | {:error, :catalogue_unavailable | :not_current | any()}
```

Applies `Catalogue.Suppliers.revise_unit_cost/3` for a proposal, guarded.

**Currency caveat (documented deferral):** goods receipts carry no currency
field, so receipt prices are assumed to be in the junction row's own
`currency` (single-currency deployments). The proposals card displays the
row currency next to both prices so the keeper sees what they are applying;
a currency-aware comparison requires a receipt-level currency first.

Returns `{:error, :catalogue_unavailable}` when the catalogue exports are
absent (same degradation path as `catalogue_resolver/0`).

# `catalogue_resolver`

```elixir
@spec catalogue_resolver() :: (Ecto.UUID.t(), Ecto.UUID.t() -&gt; any() | nil)
```

Returns a resolver function backed by `PhoenixKitCatalogue.Catalogue.Suppliers.active_info_for/2`.

The resolver is guarded: when the catalogue module is not loaded or
`active_info_for/2` is not exported, it returns `nil` for every call so
the warehouse degrades gracefully on older catalogue releases.

# `derive`

```elixir
@spec derive(
  lines :: [map()],
  supplier_uuid :: Ecto.UUID.t() | nil,
  resolver :: (Ecto.UUID.t(), Ecto.UUID.t() -&gt; any() | nil)
) :: [map()]
```

Derives price proposals from receipt lines.

Arguments:

- `lines` — list of receipt line maps (JSON keys: `"item_uuid"`, `"unit_value"`,
  `"name"`, `"sku"`).
- `supplier_uuid` — the receipt's supplier UUID; `nil` yields no proposals.
- `resolver` — a 2-arity function `(item_uuid, supplier_uuid) → info | nil`.
  Called once per eligible line. Return `nil` to skip a pair (no junction row).

A line is eligible when both `item_uuid` and a numeric `unit_value` are
present. `unit_value` may be a `Decimal`, string, integer, or float.

A proposal is generated when the resolver returns a non-nil info struct
**and** `Decimal.compare(unit_value, info.unit_cost || 0) != :eq`.

Returns a list of proposal maps with keys:
- `:item_uuid`
- `:name`
- `:sku`
- `:info` — the junction row (pass back to `Catalogue.Suppliers.revise_unit_cost/3`)
- `:current_cost` — `info.unit_cost` (may be `nil` when not yet set)
- `:receipt_price` — the `unit_value` from the line as a `Decimal`

---

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