Configuration of Mandatory and Optional Products in 1C-Bitrix Bundle
A "Gaming PC" bundle must be sold with a system unit, but keyboard, mouse, and headset are customer's choice — to include or not. This is not just a database flag: different configurations result in different prices, different inventory levels, different order lines. The standard b_catalog_product_set mechanism has an IS_REQUIRED field, but frontend option selection logic isn't implemented out of the box.
IS_REQUIRED field in b_catalog_product_set table
The IS_REQUIRED field accepts values 0 (optional) and 1 (mandatory). This is the only division in the standard structure. For more complex scenarios — for example, "select exactly one item from a group" — the schema needs extension.
We add a GROUP_ID field to b_catalog_product_set via a custom field or via table extension (if update policies allow). Alternative without changing table structure: store grouping in a separate table bl_bundle_option_group:
CREATE TABLE bl_bundle_option_group (
id SERIAL PRIMARY KEY,
bundle_id INT NOT NULL,
group_code VARCHAR(100) NOT NULL,
group_name VARCHAR(255),
min_select SMALLINT DEFAULT 0, -- minimum selected from group
max_select SMALLINT DEFAULT 1, -- maximum selected from group
item_ids INT[] NOT NULL, -- array of ITEM_ID from b_catalog_product_set
UNIQUE (bundle_id, group_code)
);
Frontend selection logic
The bundle page displays mandatory items as non-removable (checked without option to uncheck) and optional items as checkboxes or radio buttons (if max_select = 1 in the group).
When selection changes, the script recalculates the total price. Each optional component price is taken from b_catalog_price by its PRODUCT_ID. It's important to account for user-specific pricing: if a buyer has a price group, check b_catalog_price.CATALOG_GROUP_ID.
function recalcBundle(selectedItems) {
const basePrice = bundleData.basePrice; // price of mandatory items
let optionsTotal = 0;
selectedItems.forEach(itemId => {
optionsTotal += bundleData.items[itemId].price;
});
document.getElementById('bundle-total').textContent =
formatPrice(basePrice + optionsTotal);
}
Adding to cart with selected options
When adding to cart, you must submit the bundle composition — only selected items. Via AJAX POST send bundle_id and array selected_items[]. On the server:
// Form composition for cart addition
$bundle = new \Bitrix\Sale\BasketItem();
// Always take mandatory items
$requiredItems = \Bitrix\Catalog\ProductSetTable::getList([
'filter' => ['SET_ID' => $bundleId, 'IS_REQUIRED' => 1],
])->fetchAll();
// Optional items — only from $_POST['selected_items']
$optionalItems = array_intersect(
array_column($requiredItems, 'ITEM_ID'),
(array)$_POST['selected_items']
);
The final composition is passed to \Bitrix\Catalog\Product\Bundle::add(). Each component creates a separate row in b_sale_basket with the SET_PARENT_ID flag.
Inventory with optional items
Bundle availability with optional items: check inventory only for mandatory components. Optional items — separately, for each show "N pieces available" next to the checkbox. If an optional item is out of stock — checkbox is disabled, but the bundle without it can be purchased.
Bundle price with discount
Bundle discount applies only when all optional items are selected (complete configuration) — a common business case. Implemented via cart rule: discount triggers when the set of items in cart matches the full bundle composition. Rule condition: all ITEM_ID from b_catalog_product_set are present in cart with the correct quantities.
What to configure
-
bl_bundle_option_grouptable for grouping options withmin_select/max_selectparameters - Bundle display component template with checkboxes and price recalculation
- AJAX handler for cart addition with selected options submission
- Logic for checking inventory of mandatory and optional items separately
- Cart rule for full configuration discount







