Development of "Collect a Set" Functionality on 1C-Bitrix
"Collect a set" is a lighter variation of a constructor: user doesn't configure component compatibility, just selects N products from a fixed pool, getting a discount for completeness. Common cases: "Choose 3 from 10 cosmetics", "Build a pizza + drink + dessert", "Gift box: choose 5 candies from 30 flavors". Task seems simpler than PC constructor, but implementation has non-trivial moments.
Functionality Structure
"Collect a set" consists of three components:
- Set page — action description, rules (how many to select, fixed or variable price)
- Selection interface — grid of available products with checkboxes/add buttons
- Set basket — mini-basket on page showing current choice and progress
Set Data Storage
Set described by infoblock element (or custom table record):
// Properties of "Sets" infoblock
'MIN_ITEMS' => 3, // minimum for discount activation
'MAX_ITEMS' => 5, // maximum in set
'SET_PRICE' => 1990, // fixed set price (if set)
'DISCOUNT_PCT' => 15, // discount on selected sum (alternative)
'PRODUCTS' => [12, 15, 18, 22, ...], // IDs of allowed products
'ACTIVE_FROM' => '2025-03-01', // action period
'ACTIVE_TO' => '2025-04-01',
For large product pools (50+), allowed list set via section or tag filter:
'ALLOWED_SECTIONS' => [5, 7, 12], // section IDs to choose from
'ALLOWED_TAGS' => ['promo-may', 'gift-set'],
Selection Interface: Key UX Patterns
Progress counter — shows "Selected 2 from 3". Without it user gets lost. Implementation:
const maxItems = 3;
let selected = [];
function toggleProduct(productId, btn) {
if (selected.includes(productId)) {
selected = selected.filter(id => id !== productId);
btn.classList.remove('selected');
} else if (selected.length < maxItems) {
selected.push(productId);
btn.classList.add('selected');
}
updateProgress();
}
function updateProgress() {
document.querySelector('.progress-text').textContent = `Selected ${selected.length} from ${maxItems}`;
document.querySelector('.add-to-cart-btn').disabled = selected.length < minItems;
}
Blocking extra selections — when max reached, remaining items become inactive (disabled) but selected can be deselected. Visually: card gray, button inactive.
Set preview — right (desktop) or bottom (mobile) fixed panel showing selected items, final price, "To cart" button.
Price Calculation
Two pricing modes:
Fixed set price. User selected any 3 items — pays 999 rubles, regardless of individual prices. Cart adds special "Bundle" position with price 999, no itemization.
Discount on items. Sum of selected prices multiplied by coefficient (e.g., ×0.85 with 15% discount). Cart adds each item at reduced price.
For second variant use cart rules (b_sale_discount), but easier to set price programmatically via item PRICE field and CUSTOM_PRICE => 'Y'.
Adding to Cart
// AJAX request handler for adding set to cart
public function addSetToCartAction(int $setId, array $productIds): array {
// 1. Validate: all productIds allowed for this set
$setData = $this->loadSetData($setId);
if (!$this->validateProducts($productIds, $setData)) {
return ['success' => false, 'error' => 'Invalid products'];
}
// 2. Check quantity
if (count($productIds) < $setData['MIN_ITEMS'] || count($productIds) > $setData['MAX_ITEMS']) {
return ['success' => false, 'error' => 'Wrong quantity'];
}
// 3. Calculate prices
$prices = $this->calcPrices($productIds, $setData);
// 4. Add to cart
$setCode = 'bundle_' . $setId . '_' . uniqid();
$basket = \Bitrix\Sale\Basket::loadItemsForFUser(\CSaleBasket::GetBasketUserID(), SITE_ID);
foreach ($productIds as $i => $pid) {
$item = $basket->createItem('catalog', $pid);
$item->setFields([
'QUANTITY' => 1,
'CUSTOM_PRICE' => 'Y',
'PRICE' => $prices[$i],
'BASE_PRICE' => $prices[$i],
]);
// Write SET_CODE to props for cart grouping
$propCol = $item->getPropertyCollection();
$propCol->setProperty(['CODE' => 'SET_CODE', 'VALUE' => $setCode]);
}
$basket->save();
return ['success' => true, 'basket_count' => count($basket)];
}
Display in Cart and Order
Items from one set display as group. Template bitrix:sale.basket.basket customized: products grouped by SET_CODE, displayed with discount, editable (link back to constructor).
In order (b_sale_order_props), SET_CODE saved as item requisite — for correct returns processing and analytics.
Stock Restrictions
If item from set out of stock, can't select it. Check via CCatalogProduct::GetByID() or \Bitrix\Catalog\ProductTable:
$product = \Bitrix\Catalog\ProductTable::getRow([
'filter' => ['ID' => $productId],
'select' => ['QUANTITY', 'QUANTITY_TRACE', 'CAN_BUY_ZERO'],
]);
$available = ($product['CAN_BUY_ZERO'] === 'Y') || ($product['QUANTITY'] > 0);
Stock cached for 5–10 minutes — too frequent queries strain system with large pool.
Analytics
Set is conversion tool, effectiveness must be measured. Track:
- Users who started selection (page view)
- Completed selection and added to cart
- Reached payment
- Average set composition
Events sent to Yandex.Metrica or Google Analytics via dataLayer.push() on each step.
Timeframes
| Option | What's Included | Timeframe |
|---|---|---|
| Simple set (fixed price) | Selection UI + cart + set page | 1–2 weeks |
| With discounts + restrictions | + discount calculation, stock, analytics | 2–4 weeks |
| Multiple active sets | + admin management, action periods | 3–5 weeks |
"Collect a set" works best as limited action: deadline creates urgency, choice creates personalization. This combo converts better than static pre-assembled set at same price.







