Development of a configuration calculator 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

Configuration Calculator Development for 1C-Bitrix

A configuration calculator solves a problem that the standard 1C-Bitrix catalogue does not handle: the user configures a product from a set of interdependent options, sees the final price in real time and adds the fully assembled configuration to the cart. Relevant for car dealers, furniture manufacturers, IT integrators and industrial equipment suppliers.

Difference Between Product Variants and a Configuration

In the standard Bitrix catalogue, trade offers (SKU) cover fixed combinations of attributes. A configuration is a dynamic assembly where:

  • options are interdependent: selecting the "Luxury" package includes options A, B, C and makes option D unavailable
  • the final price is the sum of components, not a fixed SKU price
  • the result is a set of items for the cart, not a single product

Data Model

HL block ConfiguratorComponents:

Field Type Description
UF_PRODUCT_ID Int Base product ID in the catalogue
UF_GROUP_CODE String Option group (engine, color, wheels)
UF_OPTION_CODE String Option code
UF_OPTION_NAME String Display name
UF_BASE_PRICE Float Option price
UF_PRICE_TYPE Enum fixed / percent / delta
UF_INCOMPATIBLE String Incompatible option codes (comma-separated)
UF_REQUIRED_WITH String Options required when this one is selected

HL block ConfiguratorPresets — ready-made configurations (Basic, Standard, Luxury):

Field Type Description
UF_PRODUCT_ID Int Base product
UF_PRESET_CODE String Preset code
UF_PRESET_NAME String Name
UF_OPTIONS_JSON Text JSON with selected options
UF_DISCOUNT_PERCENT Float Preset discount

Configurator Core: Dependency Management

The central problem of a configurator is synchronising dependencies. When an option is selected: incompatible options from other groups must be deselected, required linked options must be added automatically, and the final price must be recalculated.

class ProductConfigurator {
    constructor(basePrice, components) {
        this.basePrice  = basePrice;
        this.components = components;
        this.selected   = {};
        this.graph      = this.buildIncompatibilityGraph();
    }

    buildIncompatibilityGraph() {
        const graph = {};
        this.components.forEach(c => {
            if (c.uf_incompatible) {
                graph[c.uf_option_code] = c.uf_incompatible.split(',').map(s => s.trim());
            }
        });
        return graph;
    }

    selectOption(groupCode, optionCode) {
        const blocked = this.graph[optionCode] || [];
        Object.keys(this.selected).forEach(g => {
            if (blocked.includes(this.selected[g])) delete this.selected[g];
        });

        this.selected[groupCode] = optionCode;

        const c = this.components.find(
            x => x.uf_group_code === groupCode && x.uf_option_code === optionCode
        );
        if (c && c.uf_required_with) {
            c.uf_required_with.split(',').forEach(code => {
                const req = this.components.find(x => x.uf_option_code === code.trim());
                if (req) this.selected[req.uf_group_code] = req.uf_option_code;
            });
        }

        return this.calculate();
    }

    calculate() {
        let total = this.basePrice;
        const breakdown = [];

        Object.entries(this.selected).forEach(([group, code]) => {
            const c = this.components.find(
                x => x.uf_group_code === group && x.uf_option_code === code
            );
            if (!c) return;

            let price = 0;
            if (c.uf_price_type === 'fixed')   price = c.uf_base_price;
            if (c.uf_price_type === 'percent')  price = this.basePrice * c.uf_base_price / 100;
            if (c.uf_price_type === 'delta')    price = c.uf_base_price;

            total += price;
            breakdown.push({ group, name: c.uf_option_name, price });
        });

        return { basePrice: this.basePrice, totalPrice: Math.round(total), breakdown };
    }
}

Presets as Entry Points

Most users do not configure from scratch. Ready-made presets serve as a starting point with the ability to fine-tune. A discounted preset encourages choosing a ready package instead of manually assembling from parts:

function applyPreset(configurator, preset) {
    const options = JSON.parse(preset.uf_options_json);
    configurator.selected = {};
    Object.entries(options).forEach(([group, code]) => {
        configurator.selected[group] = code;
    });
    const result = configurator.calculate();
    if (preset.uf_discount_percent > 0) {
        result.totalPrice = Math.round(result.totalPrice * (1 - preset.uf_discount_percent / 100));
        result.presetDiscount = preset.uf_discount_percent;
    }
    return result;
}

Adding a Configuration to the Bitrix Cart

public function addToCart(int $baseProductId, array $selectedOptions): \Bitrix\Main\Result
{
    $basket = \Bitrix\Sale\Basket::loadItemsForFUser(
        \Bitrix\Sale\Fuser::getId(),
        \Bitrix\Main\Context::getCurrent()->getSite()
    );

    $item = $basket->createItem('catalog', $baseProductId);
    $item->setFields([
        'QUANTITY' => 1,
        'PROPS' => [[
            'NAME'  => 'Configuration',
            'CODE'  => 'CONFIGURATION',
            'VALUE' => json_encode($selectedOptions),
        ]]
    ]);

    foreach ($selectedOptions as $option) {
        if (!empty($option['product_id'])) {
            $optItem = $basket->createItem('catalog', $option['product_id']);
            $optItem->setFields([
                'QUANTITY'     => 1,
                'PRICE'        => $option['price'],
                'CUSTOM_PRICE' => 'Y',
            ]);
        }
    }

    return $basket->save();
}

Saving User Configurations

For authenticated users — a HighLoad block SavedConfigurations:

$hash = md5($productId . json_encode($selectedOptions));

$existing = $SavedConfig::getList([
    'filter' => ['=UF_USER_ID' => $USER->GetID(), '=UF_HASH' => $hash]
])->fetch();

if (!$existing) {
    $SavedConfig::add([
        'UF_USER_ID'    => $USER->GetID(),
        'UF_PRODUCT_ID' => $productId,
        'UF_OPTIONS'    => json_encode($selectedOptions),
        'UF_PRICE'      => $totalPrice,
        'UF_HASH'       => $hash,
    ]);
}

For guests — sessionStorage or cookies with a limited lifetime.

Development Timelines

Scale Description Timeline
Basic Up to 5 option groups, no dependencies, presets 5–8 days
Standard 5–15 groups, incompatibilities, save configuration 2–3 weeks
Complex 15+ groups, cart line items, configuration history 4–8 weeks
Production Integration with 1C for live prices and stock 2–4 months

The main technical challenge is the dependency graph with a large number of options. With 50+ variants, manual testing is not feasible: automated tests for compatibility logic are needed from the first day of development.