PhoenixKit.Modules.Shop.Options (phoenix_kit v1.7.69)

Copy Markdown View Source

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

Summary

Functions

Adds a single option to category schema.

Adds a single option to global schema.

Adds a new value to an existing global option.

Builds option slots structure for product metadata.

Calculates total price modifier for selected specifications.

Calculates total modifier amount (for backward compatibility).

Gets all selectable options for admin product detail view.

Gets category-specific option schema.

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

Gets the price modifier for overridden values from product metadata.

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

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

Gets enabled global options only.

Gets a single global option by key.

Gets global option schema.

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

Returns option by key from a schema.

Gets merged option schema for a product.

Gets price-affecting options from a schema.

Gets price-affecting options for a specific product.

Gets the price modifier for a specific option value.

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

Gets all selectable options from a schema.

Gets all selectable options for a specific product.

Gets slot-based options for a product.

Checks if an option key exists in schema.

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

Removes an option from category schema by key.

Removes an option from global schema by key.

Resolves a single slot definition to a full option spec.

Updates category option schema.

Updates global option schema.

Validates product metadata against option schema.

Functions

add_category_option(category, opt)

Adds a single option to category schema.

add_global_option(opt)

Adds a single option to global schema.

add_value_to_global_option(key, value_or_map)

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

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(specs, selected_specs, base_price, product_metadata \\ %{})

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(specs, selected_specs)

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

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

Gets category-specific option schema.

get_complete_option_schema_for_product(product)

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(metadata, option_key, option_value)

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(opt, selected_value, metadata)

get_effective_modifier_info(opt, selected_value, metadata)

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(specs, metadata)

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

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

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

Gets global option schema.

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

get_modifier_range(specs)

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

Returns {min_total, max_total} as Decimals.

get_option_by_key(schema, key)

Returns option by key from a schema.

get_option_schema_for_product(product)

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

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

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(arg1, value)

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(specs, base_price, product_metadata \\ %{})

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

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

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

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?(schema, key)

Checks if an option key exists in schema.

merge_schemas(base, override)

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

remove_category_option(category, key)

Removes an option from category schema by key.

remove_global_option(key)

Removes an option from global schema by key.

resolve_slot_to_option(slot)

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(category, options)

Updates category option schema.

update_global_options(options)

Updates global option schema.

Examples

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

validate_metadata(metadata, schema)

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"}]}