Development of "assemble a set" functionality on 1C-Bitrix

Our company is engaged in the development, support and maintenance of Bitrix and Bitrix24 solutions of any complexity. From simple one-page sites to complex online stores, CRM systems with 1C and telephony integration. The experience of developers is confirmed by certificates from the vendor.
Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1175
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    811
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Development based on Bitrix, Bitrix24, 1C for the company Development of an Online Appointment Booking Widget for a Medical Center
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    747
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    976

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:

  1. Set page — action description, rules (how many to select, fixed or variable price)
  2. Selection interface — grid of available products with checkboxes/add buttons
  3. 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.