PhoenixKitCatalogue. Catalogue. SupplierFields
(PhoenixKitCatalogue v0.18.0)
Copy Markdown
View Source
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.
Summary
Functions
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.
The supplier-fields blueprint, or nil when entities is off or it has
never been provisioned. A read — never creates.
One built-in field definition by key, or nil.
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.
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}.
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.
Registers the supplier-fields deletion guard with entities. Ships as a
supervision child via PhoenixKitCatalogue.children/0.
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.
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.
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.
One field definition by key, or nil.
Extra-field types the supplier field editor offers.
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.
The blueprint's owner key, as stored in settings["managed_by"].
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.
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.
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.
One stored value by field key, or nil.
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 %{}.
Functions
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.
The supplier-fields blueprint, or nil when entities is off or it has
never been provisioned. A read — never creates.
One built-in field definition by key, or nil.
@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.
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.
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.
@spec child_spec(keyword()) :: Supervisor.child_spec()
Registers the supplier-fields deletion guard with entities. Ships as a
supervision child via PhoenixKitCatalogue.children/0.
@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.
@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.
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.
One field definition by key, or nil.
@spec field_types() :: [String.t()]
Extra-field types the supplier field editor offers.
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.
@spec owner() :: String.t()
The blueprint's owner key, as stored in settings["managed_by"].
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.
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.
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.
One stored value by field key, or nil.
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 %{}.