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.







