# `PhoenixKitCatalogue.Catalogue.SupplierFields`
[🔗](https://github.com/BeamLabEU/phoenix_kit_catalogue/blob/v0.18.0/lib/phoenix_kit_catalogue/catalogue/supplier_fields.ex#L1)

Admin-defined EXTRA fields on a supplier row — "incoterm", "carton
quantity", "certification expiry" — added without a migration.

Same doctrine as attribute-set extras
(`PhoenixKitCatalogue.Catalogue.AttributeSets`), one blueprint
narrower: entities owns the **field definitions**, the catalogue owns
the **values**.

## The blueprint contract

    name:      "catalogue_supplier_fields"   (immutable, singleton)
    settings:  "managed_by"  => "catalogue_supplier"
               "locked_keys" => ["scope"]
               "catalogue_supplier" => %{"scope" => "supplier_info"}
    records:   NONE — this blueprint is a field-definition carrier
               only. Values live on the supplier rows themselves.

A singleton: every `phoenix_kit_cat_item_supplier_info` row in the
install shares one field set, so a field added while editing one item
appears on every item's suppliers.

## Why the values are NOT entity data records

Supplier rows are per-item, high-count and relational, and they carry
money. `phoenix_kit_cat_item_supplier_info` keeps its typed columns —
`unit_cost NUMERIC(14,4)`, the `is_primary` partial unique index, real
`date` validity columns, the `{supplier_uuid, supplier_source}`
federated reference of ADR-0001. Entities casts numbers through
`Float.parse/1` and enforces nothing at the database level, so moving
those would trade exact money for JSON floats and silently drop
"one primary supplier per item".

The extras ride `metadata["custom_fields"]` instead — namespaced so an
admin-invented key can never collide with a future system key on the
same JSONB column. This is the fallback the attribute-sets design doc
named explicitly: *"keep catalogue tables with a JSONB extras column."*

## Owner string

`"catalogue_supplier"`, deliberately NOT `"catalogue"`. `AttributeSets`
enumerates its sets with `Managed.owner(&1) == "catalogue"`; sharing the
owner would make this blueprint show up as an attribute set and put it
under the set deletion guard. A distinct owner keeps both registries
clean and gives this blueprint its own guard.

## Enablement

Requires the entities module. Writes return `{:error, :entities_disabled}`
when it is off; reads degrade quietly (`[]`, `%{}`, `nil`) so the
supplier UI renders without extras rather than crashing mid-toggle.

# `add_field`

```elixir
@spec add_field(
  map(),
  keyword()
) :: {:ok, struct()} | {:error, term()}
```

Adds a field: `:label` required, `:type` one of `field_types/0`,
`select` additionally needs a non-empty `:options`. The key is derived
from the label and is stable — stored values reference it, so it never
changes afterwards.

# `blueprint`

```elixir
@spec blueprint(keyword()) :: struct() | nil
```

The supplier-fields blueprint, or `nil` when entities is off or it has
never been provisioned. A read — never creates.

# `builtin_field`

```elixir
@spec builtin_field(String.t()) :: map() | nil
```

One built-in field definition by key, or `nil`.

# `builtin_fields`

```elixir
@spec builtin_fields() :: [map()]
```

The catalogue's own supplier field definitions, in entities format.
Always available — these do not depend on the entities module being
enabled, only on its field machinery being compiled in.

# `cast_builtin`

```elixir
@spec cast_builtin(String.t(), term()) :: {:ok, term()} | {:error, term()}
```

Casts a built-in field's raw form input through the entities pipeline.
Returns `{:ok, term}` — a `%Decimal{}` for `unit_cost`, or `nil` when
cleared — or `{:error, :invalid_value}`.

Unlike the admin-defined extras, the result is destined for a typed
COLUMN, so the caller hands it straight to the changeset.

# `cast_values`

```elixir
@spec cast_values(
  map() | nil,
  keyword()
) :: {:ok, map() | nil} | {:error, term()}
```

Casts raw form input against the defined fields, through the same
entities pipeline the full form uses (`FormBuilder.cast_field/2`):
`"12.5"` coerces to `12.5`, `""` clears, a select value outside its
options is refused.

Unknown keys return `{:error, :unknown_field}` and invalid content
`{:error, :invalid_value}` — never a silent junk write. `nil` in
means "not submitted", and passes through untouched.

# `child_spec`

```elixir
@spec child_spec(keyword()) :: Supervisor.child_spec()
```

Registers the supplier-fields deletion guard with entities. Ships as a
supervision child via `PhoenixKitCatalogue.children/0`.

# `deletion_guard`

```elixir
@spec deletion_guard(struct()) :: :ok | {:error, :supplier_fields_defined}
```

Refuses deletion of the blueprint while it still defines fields —
supplier rows store values keyed by those definitions, and dropping
the blueprint would strand every one of them unreadable. An empty
blueprint is disposable; the module re-provisions it on next use.

# `enabled?`

```elixir
@spec enabled?() :: boolean()
```

True when the feature is live: the entities module is enabled AND its
package carries the Managed API. Mirrors
`AttributeSets.enabled?/0` — UI branches on this to decide whether to
render the extras at all.

# `ensure_blueprint`

```elixir
@spec ensure_blueprint(keyword()) :: {:ok, struct()} | {:error, term()}
```

The blueprint, provisioning it on first use. Idempotent: a concurrent
create loses the unique-name race and is re-read rather than reported
as an error.

# `field`

```elixir
@spec field(
  String.t(),
  keyword()
) :: map() | nil
```

One field definition by key, or `nil`.

# `field_types`

```elixir
@spec field_types() :: [String.t()]
```

Extra-field types the supplier field editor offers.

# `fields`

```elixir
@spec fields(keyword()) :: [map()]
```

The defined field definitions (entities `fields_definition` entries,
string keys). `[]` when entities is off or nothing is defined — so
every render site can iterate unconditionally.

# `owner`

```elixir
@spec owner() :: String.t()
```

The blueprint's owner key, as stored in `settings["managed_by"]`.

# `put_values`

```elixir
@spec put_values(map() | nil, map() | nil) :: map()
```

Merges cast values into a row's `metadata`, under the namespaced key.
Returns the metadata map to hand to the changeset. A `nil` `values`
leaves metadata untouched, so a caller that didn't collect extras
cannot blank the ones already stored.

# `remove_field`

```elixir
@spec remove_field(
  String.t(),
  keyword()
) :: {:ok, struct()} | {:error, term()}
```

Removes a field definition. Values already stored under its key are
left in place — harmless and invisible, and they come back if a field
with the same key is re-added. Same doctrine as entities' own field
removal and attribute-set extras.

# `update_field`

```elixir
@spec update_field(String.t(), map(), keyword()) :: {:ok, struct()} | {:error, term()}
```

Updates a field: `:label` renames the display text, `:options`
replaces a select's choices. The KEY and the TYPE are immutable —
stored values were cast for that type and are addressed by that key.

# `value`

```elixir
@spec value(struct() | nil, String.t()) :: term()
```

One stored value by field key, or `nil`.

# `values`

```elixir
@spec values(struct() | nil) :: map()
```

The custom-field values carried by one supplier-info row, as a
`%{key => value}` map. Always a map — a row written before any field
existed reads as `%{}`.

---

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