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.
Summary
Functions
Applies Catalogue.Suppliers.revise_unit_cost/3 for a proposal, guarded.
Returns a resolver function backed by PhoenixKitCatalogue.Catalogue.Suppliers.active_info_for/2.
Derives price proposals from receipt lines.
Functions
@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).
@spec catalogue_resolver() :: (Ecto.UUID.t(), Ecto.UUID.t() -> 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.
@spec derive( lines :: [map()], supplier_uuid :: Ecto.UUID.t() | nil, resolver :: (Ecto.UUID.t(), Ecto.UUID.t() -> 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;nilyields no proposals.resolver— a 2-arity function(item_uuid, supplier_uuid) → info | nil. Called once per eligible line. Returnnilto 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 toCatalogue.Suppliers.revise_unit_cost/3):current_cost—info.unit_cost(may benilwhen not yet set):receipt_price— theunit_valuefrom the line as aDecimal