# `PhoenixKit.Modules.Shop.Options`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L1)

Context for managing product options.

Provides a two-level option system:
- **Global options** - Apply to all products (stored in shop_config)
- **Category options** - Apply to products in specific category (stored in category.option_schema)

When retrieving options for a product, the system merges global and category options,
with category options overriding global ones by key.

## Localization Note

Option labels and values are currently stored as plain strings, not localized JSONB maps.
This means options display the same in all languages. Future enhancement: convert
option schema to support localized labels like `"label" => %{"en" => "Material", "ru" => "Материал"}`.

## Usage

    # Get/set global options
    Options.get_global_options()
    Options.update_global_options([%{"key" => "material", "label" => "Material", "type" => "text"}])

    # Get/set category options
    Options.get_category_options(category)
    Options.update_category_options(category, [%{"key" => "mounting_type", ...}])

    # Get merged schema for a product
    Options.get_option_schema_for_product(product)

    # Validate product metadata against schema
    Options.validate_metadata(product.metadata, schema)

## Price Calculation

Options with `affects_price: true` can modify the final product price.
Two modifier types are supported:

- `fixed` - Add exact amount (e.g., +$10)
- `percent` - Add percentage of base price (e.g., +20%)

## Allow Override

Options with `allow_override: true` can have their price modifiers customized
per-product. Override values are stored in product metadata under `_price_modifiers`.
When calculating price, the system checks for overrides first, then falls back
to the default values from the option schema.

Calculation order:
1. Sum all fixed modifiers (checking for overrides)
2. Add to base price (intermediate price)
3. Sum all percent modifiers (checking for overrides)
4. Apply percent to intermediate price

Example:
- Base price: $20
- Material: PETG (+$10 fixed)
- Finish: Premium (+20% percent)
- Final: ($20 + $10) * 1.20 = $36

# `add_category_option`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L295)

Adds a single option to category schema.

# `add_global_option`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L127)

Adds a single option to global schema.

# `add_value_to_global_option`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L184)

Adds a new value to an existing global option.

Works with both simple string options and enhanced map options.
For enhanced format, value_map should be a map with at least "value" key.

## Examples

    # Simple format - adds "yellow" to options list
    Options.add_value_to_global_option("color", "yellow")

    # Enhanced format - adds map to options list
    Options.add_value_to_global_option("color", %{
      "value" => "yellow",
      "label" => %{"en" => "Yellow", "ru" => "Жёлтый"},
      "hex" => "#FFFF00"
    })

# `build_option_slots`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L500)

Builds option slots structure for product metadata.

Creates the `_option_slots` array from a list of slot definitions.

## Examples

    Options.build_option_slots([
      %{slot: "cup_color", source_key: "color", label: %{"en" => "Cup Color"}},
      %{slot: "liquid_color", source_key: "color", label: %{"en" => "Liquid"}}
    ])
    # => [
    #   %{"slot" => "cup_color", "source_key" => "color", "label" => %{"en" => "Cup Color"}},
    #   %{"slot" => "liquid_color", "source_key" => "color", "label" => %{"en" => "Liquid"}}
    # ]

# `calculate_final_price`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L1095)

Calculates total price modifier for selected specifications.

Takes a list of price-affecting options, a map of selected values, and the base price.
Returns the final price after applying all modifiers.

## Options

- `product_metadata` - Optional product metadata for custom modifier values.
  When provided, options with `modifier_type: "custom"` will use price values
  from `metadata["_price_modifiers"][option_key][option_value]`.

## Calculation Order

1. Sum all fixed modifiers (from schema price_modifiers)
2. Sum all custom modifiers (from product metadata)
3. Add to base price (intermediate price)
4. Sum all percent modifiers
5. Apply percent to intermediate price: intermediate * (1 + percent_sum/100)

## Examples

    specs = [
      %{"key" => "material", "affects_price" => true, "modifier_type" => "fixed",
        "price_modifiers" => %{"PLA" => "0", "PETG" => "10.00"}},
      %{"key" => "finish", "affects_price" => true, "modifier_type" => "percent",
        "price_modifiers" => %{"Standard" => "0", "Premium" => "20"}}
    ]

    selected = %{"material" => "PETG", "finish" => "Premium"}
    base_price = Decimal.new("20.00")

    Options.calculate_final_price(specs, selected, base_price)
    # => Decimal.new("36.00")  # ($20 + $10) * 1.20

# `calculate_total_modifier`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L1165)

Calculates total modifier amount (for backward compatibility).

This function returns just the sum of fixed modifiers.
For full calculation with percent modifiers, use `calculate_final_price/3`.

## Examples

    specs = [
      %{"key" => "material", "affects_price" => true, "price_modifiers" => %{"PETG" => "10.00"}},
      %{"key" => "color", "affects_price" => true, "price_modifiers" => %{"Gold" => "8.00"}}
    ]

    selected = %{"material" => "PETG", "color" => "Gold"}

    Options.calculate_total_modifier(specs, selected)
    # => Decimal.new("18.00")

# `get_all_selectable_specs_for_admin`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L712)

Gets all selectable options for admin product detail view.

Unlike `get_selectable_specs_for_product/1`, this does NOT filter schema options
by product's `_option_values`. Shows all schema options (global + category) plus
discovered options from metadata, giving admins the full picture.

# `get_category_options`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L257)

Gets category-specific option schema.

# `get_complete_option_schema_for_product`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L460)

Gets complete option schema for a product including slot-based options.

This combines:
1. Global options (excluding those used as slot sources)
2. Category options
3. Slot-based options from product metadata

## Examples

    Options.get_complete_option_schema_for_product(product)

# `get_custom_price_modifier`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L1042)

Gets the price modifier for overridden values from product metadata.

Used when option has `allow_override: true` and the product has custom values
stored in metadata under `_price_modifiers` key.

## Examples

    product_metadata = %{
      "_price_modifiers" => %{
        "material" => %{"PLA" => "0", "PETG" => "15.00"}
      }
    }

    Options.get_custom_price_modifier(product_metadata, "material", "PETG")
    # => Decimal.new("15.00")

# `get_effective_modifier`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L1020)

# `get_effective_modifier_info`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L962)

Gets the effective modifier info (type and value) for an option, checking for product overrides.

Returns `{modifier_type, modifier_value}` tuple.

If the option has `allow_override: true` and the product has an override in metadata,
uses the override type and value. Otherwise uses defaults from option's schema.

## Override Structure

Overrides in metadata can be:
- New format: `%{"type" => "fixed", "value" => "10.00"}` - custom type and value
- Legacy format: `"10.00"` - just value, inherits option's default type

## Examples

    # Option with custom override (type + value)
    opt = %{"key" => "material", "allow_override" => true, "modifier_type" => "fixed", ...}
    metadata = %{"_price_modifiers" => %{"material" => %{"PETG" => %{"type" => "percent", "value" => "15"}}}}
    get_effective_modifier_info(opt, "PETG", metadata)
    # => {"percent", Decimal.new("15")}

    # Option with legacy override (just value)
    metadata = %{"_price_modifiers" => %{"material" => %{"PETG" => "15.00"}}}
    get_effective_modifier_info(opt, "PETG", metadata)
    # => {"fixed", Decimal.new("15.00")}  # Uses option's default type

# `get_effective_modifier_range`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L1290)

Gets the min/max modifier range for options, considering product overrides.

For options with `allow_override: true`, checks if product has override values
in metadata and uses those instead of defaults.

Returns `{min_total, max_total}` as Decimals.

# `get_enabled_global_options`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L91)

Gets enabled global options only.

Filters out options where `enabled` is explicitly set to `false`.
Options without the `enabled` key default to enabled (backward compatible).

# `get_global_option_by_key`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L159)

Gets a single global option by key.

Returns the option definition map or nil if not found.

## Examples

    Options.get_global_option_by_key("color")
    # => %{"key" => "color", "label" => "Color", "type" => "select", ...}

# `get_global_options`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L77)

Gets global option schema.

Returns a list of option definitions that apply to all products.

# `get_modifier_range`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L1264)

Gets the min/max modifier range for a list of options.

Returns `{min_total, max_total}` as Decimals.

# `get_option_by_key`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L615)

Returns option by key from a schema.

# `get_option_schema_for_product`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L334)

Gets merged option schema for a product.

Combines global options with category-specific options.
Category options override global ones with the same key.

## Examples

    # Product with category
    schema = Options.get_option_schema_for_product(product)

    # Product without category (global only)
    schema = Options.get_option_schema_for_product(product_without_category)

# `get_price_affecting_specs`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L646)

Gets price-affecting options from a schema.

Returns only options that have `affects_price: true` and are
of type `select` or `multiselect`.

## Examples

    schema = [
      %{"key" => "material", "type" => "select", "affects_price" => true, ...},
      %{"key" => "notes", "type" => "text", ...}
    ]

    Options.get_price_affecting_specs(schema)
    # => [%{"key" => "material", ...}]

# `get_price_affecting_specs_for_product`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L776)

Gets price-affecting options for a specific product.

Combines global and category options, then filters for price-affecting ones.

If the product has `_option_values` in metadata, only returns options
for which the product has values. This allows products without certain
options (e.g., Size) to skip required validation for those options.

Additionally, discovers options from product metadata that have price modifiers
but are not defined in the schema (e.g., imported products with custom options).

# `get_price_modifier`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L922)

Gets the price modifier for a specific option value.

Returns a Decimal value representing the price delta for the selected option.
Returns Decimal.new("0") if the option has no modifier or option doesn't affect price.

For "custom" modifier type, the modifiers come from product metadata.

## Examples

    opt = %{
      "key" => "material",
      "affects_price" => true,
      "price_modifiers" => %{"PLA" => "0", "PETG" => "10.00"}
    }

    Options.get_price_modifier(opt, "PETG")
    # => Decimal.new("10.00")

    Options.get_price_modifier(opt, "PLA")
    # => Decimal.new("0")

# `get_price_range`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L1203)

Gets the min/max price range for a list of options.

For each option, finds the minimum and maximum modifier values,
then calculates the final price range considering both fixed and percent modifiers.

Returns `{min_price, max_price}` as Decimals.

## Examples

    specs = [
      %{"key" => "material", "modifier_type" => "fixed",
        "price_modifiers" => %{"PLA" => "0", "PETG" => "10.00"}},
      %{"key" => "finish", "modifier_type" => "percent",
        "price_modifiers" => %{"Standard" => "0", "Premium" => "20"}}
    ]
    base_price = Decimal.new("20.00")

    Options.get_price_range(specs, base_price)
    # => {Decimal.new("20.00"), Decimal.new("36.00")}

# `get_selectable_specs`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L672)

Gets all selectable options from a schema.

Returns options that are of type `select` or `multiselect` and not hidden.
Unlike `get_price_affecting_specs/1`, this includes options regardless of
whether they affect price. Use this for UI display of all selectable options.

## Examples

    schema = [
      %{"key" => "color", "type" => "select", "options" => ["Red", "Blue"]},
      %{"key" => "material", "type" => "select", "affects_price" => true, ...},
      %{"key" => "notes", "type" => "text", ...}
    ]

    Options.get_selectable_specs(schema)
    # => [%{"key" => "color", ...}, %{"key" => "material", ...}]

# `get_selectable_specs_for_product`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L691)

Gets all selectable options for a specific product.

Combines global and category options, then filters for select/multiselect types.
Also discovers options from product metadata that have values defined.
Unlike `get_price_affecting_specs_for_product/1`, this includes all selectable
options regardless of whether they affect price.

Use this for displaying option selectors in the product UI.

# `get_slot_options_for_product`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L390)

Gets slot-based options for a product.

Resolves `_option_slots` from product metadata to full option specs.
Each slot references a global option via `source_key` and creates a
customized option spec with the slot's key and label.

## Examples

    product.metadata = %{
      "_option_slots" => [
        %{"slot" => "cup_color", "label" => %{"en" => "Cup Color"}, "source_key" => "color"},
        %{"slot" => "liquid_color", "label" => %{"en" => "Liquid"}, "source_key" => "color"}
      ]
    }

    Options.get_slot_options_for_product(product)
    # => [
    #   %{"key" => "cup_color", "label" => %{"en" => "Cup Color"}, "type" => "select", ...},
    #   %{"key" => "liquid_color", "label" => %{"en" => "Liquid"}, "type" => "select", ...}
    # ]

# `has_option?`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L622)

Checks if an option key exists in schema.

# `merge_schemas`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L351)

Merges two option schemas, with the second overriding the first by key.

# `remove_category_option`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L310)

Removes an option from category schema by key.

# `remove_global_option`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L143)

Removes an option from global schema by key.

# `resolve_slot_to_option`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L409)

Resolves a single slot definition to a full option spec.

Takes a slot map with "slot", "label", and "source_key",
finds the referenced global option, and creates a new spec
with the slot's key and label but the source's type and values.

# `update_category_options`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L284)

Updates category option schema.

# `update_global_options`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L106)

Updates global option schema.

## Examples

    Options.update_global_options([
      %{"key" => "material", "label" => "Material", "type" => "select",
        "options" => ["PLA", "ABS", "PETG"], "default" => "PLA"}
    ])

# `validate_metadata`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.63/lib/modules/shop/options/options.ex#L540)

Validates product metadata against option schema.

Returns `:ok` or `{:error, errors}` where errors is a list of `{key, message}` tuples.

## Examples

    schema = [%{"key" => "material", "type" => "select", "options" => ["PLA", "ABS"], "required" => true}]

    Options.validate_metadata(%{"material" => "PLA"}, schema)
    # => :ok

    Options.validate_metadata(%{}, schema)
    # => {:error, [{"material", "is required"}]}

    Options.validate_metadata(%{"material" => "Invalid"}, schema)
    # => {:error, [{"material", "must be one of: PLA, ABS"}]}

---

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