PhoenixKitWarehouse.CostProposals (PhoenixKitWarehouse v0.2.4)

Copy Markdown View Source

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.

Derives price proposals from receipt lines.

Functions

apply_revision(map, opts)

@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()

@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.

derive(lines, supplier_uuid, resolver)

@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; 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_costinfo.unit_cost (may be nil when not yet set)
  • :receipt_price — the unit_value from the line as a Decimal