# Understanding Modifiers Modifiers let customers customize menu items -- choosing bread type, adding toppings, selecting a preparation style. In the Tote API, modifiers are organized into **modifier groups** that can nest up to **3 levels deep**, enabling complex customization trees like "Sandwich > Steak > Medium > Chimichurri Sauce." This guide explains the modifier data model, walks through a fully annotated 3-level worked example, and covers pricing, validation, default selections, and UI rendering tips. ## What Are Modifiers? A **modifier** is a customization option for a menu item (e.g., "Cheddar Cheese", "Medium", "A1 Steak Sauce"). A **modifier group** is a named collection of modifiers with selection rules (e.g., "Bread Choice: pick exactly 1", "Toppings: pick up to 5"). Menu items contain modifier groups. Each modifier within a group can itself contain nested modifier groups, creating a tree structure. The API supports **up to 3 levels** of nesting. ``` Menu Item |-- Modifier Group (Level 1) | |-- Modifier | | |-- Modifier Group (Level 2) | | |-- Modifier | | | |-- Modifier Group (Level 3) | | | |-- Modifier | | | |-- Modifier | | |-- Modifier | |-- Modifier |-- Modifier Group (Level 1) |-- Modifier |-- Modifier ``` ## Modifier Group Rules Every modifier group defines selection constraints using three fields: | Field | Type | Description | | --- | --- | --- | | `min_selections` | integer | Minimum modifiers the customer must select. `0` = optional. | | `max_selections` | integer | Maximum modifiers the customer may select. | | `allows_duplicates` | boolean | Whether the same modifier can be selected more than once. | ### Common Patterns | Pattern | min | max | duplicates | Example | | --- | --- | --- | --- | --- | | Required single-select | 1 | 1 | false | Bread Choice (pick exactly 1) | | Required multi-select | 1 | 3 | false | Protein (pick 1 to 2) | | Optional multi-select | 0 | 5 | false | Toppings (pick up to 5) | | Optional with duplicates | 0 | 3 | true | Extra Cheese (add up to 3x) | | Required with duplicates | 1 | 10 | true | Scoops (pick 1 to 10, same flavor OK) | ## Worked Example: Build Your Own Sub Sandwich This is the centerpiece of the modifier system. We will walk through a real menu item from the API with all 3 levels of nesting, showing the JSON at each level. ### The Menu Item The "Build Your Own Sub Sandwich" has three modifier groups at Level 1: ```json { "id": "d0000001-0000-0000-0000-000000000001", "name": "Build Your Own Sub Sandwich", "description": "Choose your bread, protein, and toppings.", "base_price": { "amount": 899, "currency": "USD" }, "available": true, "modifier_groups": [ { "name": "Bread Choice", "..." : "Level 1 group" }, { "name": "Protein", "..." : "Level 1 group" }, { "name": "Toppings", "..." : "Level 1 group" } ] } ``` Base price: **$8.99** (899 cents). ### Level 1: Bread Choice (Single-Select, Required) The customer must pick exactly one bread. ```json { "id": "mg000001-0000-0000-0000-000000000001", "name": "Bread Choice", "min_selections": 1, "max_selections": 1, "allows_duplicates": false, "modifiers": [ { "id": "m0000001-0000-0000-0000-000000000001", "name": "White", "price_adjustment": { "amount": 0, "currency": "USD" }, "is_default": true, "modifier_groups": [] }, { "id": "m0000001-0000-0000-0000-000000000002", "name": "Wheat", "price_adjustment": { "amount": 0, "currency": "USD" }, "is_default": false, "modifier_groups": [] }, { "id": "m0000001-0000-0000-0000-000000000003", "name": "Italian Herb & Cheese", "price_adjustment": { "amount": 75, "currency": "USD" }, "is_default": false, "modifier_groups": [] } ] } ``` **Key observations:** - `min_selections: 1` and `max_selections: 1` -- the customer must pick exactly one bread. - White is `is_default: true` -- pre-selected in the UI. - White and Wheat cost $0 extra. Italian Herb & Cheese adds $0.75 (75 cents). - `modifier_groups: []` -- no further nesting. This is a leaf-level group. ### Level 1: Protein (Multi-Select, Required) The customer must pick 1 to 2 proteins. ```json { "id": "mg000001-0000-0000-0000-000000000002", "name": "Protein", "min_selections": 1, "max_selections": 2, "allows_duplicates": false, "modifiers": [ { "id": "m0000002-0000-0000-0000-000000000001", "name": "Turkey", "price_adjustment": { "amount": 0, "currency": "USD" }, "is_default": false, "modifier_groups": [] }, { "id": "m0000002-0000-0000-0000-000000000002", "name": "Ham", "price_adjustment": { "amount": 0, "currency": "USD" }, "is_default": false, "modifier_groups": [] }, { "id": "m0000002-0000-0000-0000-000000000003", "name": "Steak", "price_adjustment": { "amount": 200, "currency": "USD" }, "is_default": false, "modifier_groups": [ { "name": "Steak Preparation", "..." : "Level 2 group (see below)" } ] } ] } ``` **Key observations:** - `max_selections: 2` -- the customer can pick up to two proteins (e.g., Turkey + Ham). - Turkey and Ham cost $0 extra. Steak adds $2.00 (200 cents). - **Steak has a nested modifier group** -- selecting Steak unlocks "Steak Preparation" at Level 2. - Turkey and Ham have `modifier_groups: []` -- they are leaf-level modifiers. ### Level 2: Steak Preparation (Single-Select, Required) When the customer selects Steak, they must choose a preparation level. ```json { "id": "mg000002-0000-0000-0000-000000000001", "name": "Steak Preparation", "min_selections": 1, "max_selections": 1, "allows_duplicates": false, "modifiers": [ { "id": "m0000003-0000-0000-0000-000000000001", "name": "Rare", "price_adjustment": { "amount": 0, "currency": "USD" }, "is_default": false, "modifier_groups": [] }, { "id": "m0000003-0000-0000-0000-000000000002", "name": "Medium", "price_adjustment": { "amount": 0, "currency": "USD" }, "is_default": true, "modifier_groups": [ { "name": "Sauce", "..." : "Level 3 group (see below)" } ] }, { "id": "m0000003-0000-0000-0000-000000000003", "name": "Well Done", "price_adjustment": { "amount": 0, "currency": "USD" }, "is_default": false, "modifier_groups": [] } ] } ``` **Key observations:** - This is a **Level 2 group** -- nested inside the Steak modifier. - Medium is `is_default: true` -- pre-selected when the customer picks Steak. - All preparations cost $0 extra (the $2.00 upcharge is on the Steak modifier itself). - **Medium has a nested modifier group** -- selecting Medium unlocks "Sauce" at Level 3. - Rare and Well Done have `modifier_groups: []` -- they are leaf-level. ### Level 3: Sauce (Multi-Select, Optional) When the customer selects Medium steak, they can optionally choose up to 2 sauces. ```json { "id": "mg000003-0000-0000-0000-000000000001", "name": "Sauce", "min_selections": 0, "max_selections": 2, "allows_duplicates": false, "modifiers": [ { "id": "m0000004-0000-0000-0000-000000000001", "name": "A1 Steak Sauce", "price_adjustment": { "amount": 0, "currency": "USD" }, "is_default": false, "modifier_groups": [] }, { "id": "m0000004-0000-0000-0000-000000000002", "name": "Peppercorn Ranch", "price_adjustment": { "amount": 50, "currency": "USD" }, "is_default": false, "modifier_groups": [] }, { "id": "m0000004-0000-0000-0000-000000000003", "name": "Chimichurri", "price_adjustment": { "amount": 75, "currency": "USD" }, "is_default": false, "modifier_groups": [] } ] } ``` **Key observations:** - This is the **deepest level** (Level 3). No further nesting is allowed. - `min_selections: 0` -- sauces are optional. - `max_selections: 2` -- the customer can pick up to 2 sauces. - A1 is free, Peppercorn Ranch adds $0.50, Chimichurri adds $0.75. - All modifiers have `modifier_groups: []` -- guaranteed at Level 3. ### Level 1: Toppings (Multi-Select, Optional) The customer can pick up to 5 toppings. This is a flat group with no nesting. ```json { "id": "mg000001-0000-0000-0000-000000000003", "name": "Toppings", "min_selections": 0, "max_selections": 5, "allows_duplicates": false, "modifiers": [ { "id": "m0000005-0000-0000-0000-000000000001", "name": "Lettuce", "price_adjustment": { "amount": 0, "currency": "USD" }, "is_default": false, "modifier_groups": [] }, { "id": "m0000005-0000-0000-0000-000000000002", "name": "Tomato", "price_adjustment": { "amount": 0, "currency": "USD" }, "is_default": false, "modifier_groups": [] }, { "id": "m0000005-0000-0000-0000-000000000003", "name": "Pickles", "price_adjustment": { "amount": 0, "currency": "USD" }, "is_default": false, "modifier_groups": [] }, { "id": "m0000005-0000-0000-0000-000000000004", "name": "Jalapenos", "price_adjustment": { "amount": 50, "currency": "USD" }, "is_default": false, "modifier_groups": [] }, { "id": "m0000005-0000-0000-0000-000000000005", "name": "Avocado", "price_adjustment": { "amount": 150, "currency": "USD" }, "is_default": false, "modifier_groups": [] } ] } ``` **Key observations:** - `min_selections: 0` -- toppings are entirely optional. - Most toppings are free. Jalapenos add $0.50. Avocado adds $1.50. - No nesting -- all modifiers are leaf-level. ### Complete Nesting Visualization ``` Build Your Own Sub Sandwich ($8.99 base) | |-- Bread Choice [pick exactly 1] | |-- White (+$0.00) *default* | |-- Wheat (+$0.00) | |-- Italian Herb & Cheese (+$0.75) | |-- Protein [pick 1 to 2] | |-- Turkey (+$0.00) | |-- Ham (+$0.00) | |-- Steak (+$2.00) | | | |-- Steak Preparation [pick exactly 1] | |-- Rare (+$0.00) | |-- Medium (+$0.00) *default* | | | | | |-- Sauce [pick 0 to 2] | | |-- A1 Steak Sauce (+$0.00) | | |-- Peppercorn Ranch (+$0.50) | | |-- Chimichurri (+$0.75) | | | |-- Well Done (+$0.00) | |-- Toppings [pick 0 to 5] |-- Lettuce (+$0.00) |-- Tomato (+$0.00) |-- Pickles (+$0.00) |-- Jalapenos (+$0.50) |-- Avocado (+$1.50) ``` ## Pricing The total price of a customized item is the base price plus all selected modifier price adjustments. Modifiers at every level contribute to the total. **Formula:** ``` total_price = base_price + sum(all selected modifier price_adjustments) ``` All monetary values are in **integer cents** (USD). ### Example Calculation A customer orders a sub with: - Italian Herb & Cheese bread (+$0.75) - Steak protein (+$2.00) - Medium preparation (+$0.00) - Chimichurri sauce (+$0.75) - Lettuce (+$0.00) - Avocado (+$1.50) ``` Base price: $8.99 (899 cents) + Italian Herb & Cheese: $0.75 ( 75 cents) + Steak: $2.00 (200 cents) + Medium: $0.00 ( 0 cents) + Chimichurri: $0.75 ( 75 cents) + Lettuce: $0.00 ( 0 cents) + Avocado: $1.50 (150 cents) ---------------------------------------------- Total: $13.99 (1399 cents) ``` **Pseudocode:** ```python def calculate_item_price(item, selected_modifiers): """ Calculate total price for an item with selected modifiers. item: MenuItem object selected_modifiers: list of Modifier objects (all levels flattened) """ total = item["base_price"]["amount"] # Start with base price in cents for modifier in selected_modifiers: total += modifier["price_adjustment"]["amount"] return total ``` > **Note:** This is a client-side estimate. Use the server-side price calculation endpoint (Phase 2) for the authoritative total, which includes tax and discounts. ## Default Modifiers Modifiers with `is_default: true` are pre-selected in the UI. Defaults provide a reasonable starting configuration without requiring the customer to make every choice. **Behavior rules:** 1. Default modifiers count toward `min_selections`. If `min_selections: 1` and a modifier is `is_default: true`, the requirement is met without customer action. 2. A group can have at most `max_selections` defaults. (If a group allows 1 selection and 2 modifiers are marked default, use only the first.) 3. Default modifiers contribute to the price. If a default modifier has a non-zero `price_adjustment`, it is included in the price from the start. 4. The customer can deselect defaults and choose alternatives. **In the worked example:** - Bread Choice: "White" is default (customer starts with White bread). - Steak Preparation: "Medium" is default (if customer picks Steak, Medium is pre-selected). - No defaults in Protein, Toppings, or Sauce groups. ## Validation Rules Before submitting modifier selections to the cart, validate them client-side to provide immediate feedback. ### Validation pseudocode ```python def validate_modifier_group(group, selections): """ Validate modifier selections for a group. group: ModifierGroup object selections: list of selected modifier IDs for this group Returns: (is_valid, error_message) """ count = len(selections) min_sel = group["min_selections"] max_sel = group["max_selections"] # Check minimum selections if count < min_sel: return (False, f"{group['name']}: select at least {min_sel} (got {count})") # Check maximum selections if count > max_sel: return (False, f"{group['name']}: select at most {max_sel} (got {count})") # Check for duplicates if not group["allows_duplicates"] and len(set(selections)) != count: return (False, f"{group['name']}: duplicate selections not allowed") # Check that selected IDs exist in the group valid_ids = {m["id"] for m in group["modifiers"]} for sel in selections: if sel not in valid_ids: return (False, f"{group['name']}: modifier {sel} not found in group") return (True, None) def validate_item_modifiers(item, all_selections): """ Validate all modifier selections for a menu item recursively. item: MenuItem object all_selections: dict mapping modifier_group_id -> list of selected modifier IDs Returns: list of (is_valid, error_message) tuples """ errors = [] def validate_groups(groups): for group in groups: group_id = group["id"] selections = all_selections.get(group_id, []) is_valid, error = validate_modifier_group(group, selections) if not is_valid: errors.append(error) # Recursively validate nested groups for selected modifiers for modifier in group["modifiers"]: if modifier["id"] in selections and modifier.get("modifier_groups"): validate_groups(modifier["modifier_groups"]) validate_groups(item.get("modifier_groups", [])) return errors ``` ### Validation rules summary | Rule | Check | Error | | --- | --- | --- | | Minimum selections | `len(selections) >= min_selections` | "Select at least N" | | Maximum selections | `len(selections) <= max_selections` | "Select at most N" | | No duplicates | If `allows_duplicates: false`, all IDs must be unique | "Duplicate selections not allowed" | | Valid modifier IDs | All selected IDs exist in the group's `modifiers` array | "Modifier not found" | | Nested groups | If a selected modifier has nested groups, validate those too | (recursive) | | Level 3 is terminal | Modifiers at Level 3 will never have `modifier_groups` | N/A (enforced by API) | ## UI Rendering Tips ### Display each nesting level progressively Do not show all 3 levels at once. Reveal nested modifier groups only after the parent modifier is selected: 1. Show Level 1 groups when the customer opens the item. 2. When the customer selects a modifier with nested groups, expand Level 2 below it. 3. When the customer selects a Level 2 modifier with nested groups, expand Level 3. ### Render group types appropriately | Pattern | min | max | UI Element | | --- | --- | --- | --- | | Exactly 1 | 1 | 1 | Radio buttons | | Up to 1 | 0 | 1 | Radio buttons + "None" option | | Multiple | 0+ | 2+ | Checkboxes | | With duplicates | any | any | Stepper/counter per modifier | ### Show price adjustments inline Display the price adjustment next to each modifier name so the customer knows the cost before selecting: ``` Bread Choice (pick 1): (*) White ( ) Wheat ( ) Italian Herb & Cheese (+$0.75) ``` Modifiers with `price_adjustment.amount: 0` should show nothing or "Included" -- not "+$0.00." ### Pre-select defaults When rendering a modifier group, check each modifier's `is_default` field. Pre-select those modifiers in the UI so the customer starts with a valid configuration. ### Show required vs. optional Mark groups with `min_selections >= 1` as "Required" in the UI. Groups with `min_selections: 0` are optional and should be visually distinct. ``` Bread Choice (Required - pick 1): ... Toppings (Optional - up to 5): ... ``` ## Next Steps - [Getting Started Guide](/online-ordering/guides/01-getting-started) -- Your first API calls. - [Menu Synchronization Guide](/online-ordering/guides/03-menu-sync) -- Keeping modifier data current. - [Authentication Guide](/online-ordering/guides/02-authentication) -- Token management for API access.