Imagine a client looking at a laptop priced at $700, sees an offer "add mouse + bag, get −15%" — and adds everything to the cart. The average order value jumps by 25–30%, and the customer feels good about the savings. But configuring such a discount in 1C-Bitrix often hits snags: the discount doesn't apply when items are added one by one, it doesn't appear on the product card, or it conflicts with other promotions. We solve these problems end-to-end in 1–4 days. You get a working mechanism with a "Collect Kit" block and honest savings shown on the frontend. Our certified specialists with 5+ years of experience ensure results — we'll assess your project for free, just reach out.
Built-in Discount for Set of Products in the Marketing Module
The built-in marketing module (Administration → Shop → Marketing → Discounts) lets you create a rule with the condition "Product Set". Specify the product IDs or SKUs, minimum quantity for each, and a percentage or fixed discount amount. This is enough for simple cases where the discount applies automatically after all items are added to the cart.
Main limitations of the built-in module:
- The condition builder is unintuitive for non-standard bundles.
- The discount is not shown on the product card until the product is in the cart.
- There is no "Collect Kit" block on the frontend.
If you only need basic logic — the marketing module works. But when you want to present the savings before checkout, a custom handler is necessary.
When the Built-in Functionality Is Not Enough
The built-in discount cannot show the customer how much they save until they enter the cart. Also, it cannot verify that items were added at the same time, not in separate orders. If you need to display a block with other bundle items on the product card, add an "Add All to Cart" button, and let the customer feel the savings right away — we go custom.
Writing a Custom Handler for the Bundle
We create a handler for the OnBeforeOrderFinalAction event. The BundleDiscount class checks that all bundle items are in the basket and applies the discount. As described in Bitrix Dev documentation (https://dev.1c-bitrix.ru/learning/course/index.php?COURSE_ID=42&CHAPTER_ID=04258), this event fires before order finalization.
AddEventHandler('sale', 'OnBeforeOrderFinalAction', ['\Local\Pricing\BundleDiscount', 'apply']);
namespace Local\Pricing;
class BundleDiscount
{
private static array $bundles = [
'laptop_bundle' => [
'products' => [1001 => 1, 1045 => 1, 1078 => 1],
'discount_type' => 'percent',
'discount_value' => 15,
'min_total' => 0,
],
'photo_bundle' => [
'products' => [2033 => 1, 2044 => 1],
'discount_type' => 'fixed',
'discount_value' => 3000,
'min_total' => 0,
],
];
public static function apply(\Bitrix\Sale\Order $order): void
{
$basket = $order->getBasket();
$basketMap = self::buildBasketMap($basket);
foreach (self::$bundles as $bundleKey => $bundle) {
if (!self::isBundlePresent($basketMap, $bundle['products'])) {
continue;
}
$discount = self::calculateDiscount($basket, $bundle);
if ($discount <= 0) continue;
$discountResult = new \Bitrix\Sale\Discount\Result\DiscountResult();
$discountResult->setApplyResult([
'DISCOUNT_VALUE' => $discount,
'DISCOUNT_TYPE' => 'F',
'DISCOUNT_RESULT' => ['BASKET' => $basketMap],
]);
$order->getDiscount()->setApplyResult($discountResult);
}
}
private static function buildBasketMap(\Bitrix\Sale\Basket $basket): array
{
$map = [];
foreach ($basket as $item) {
$map[(int)$item->getProductId()] = (int)$item->getQuantity();
}
return $map;
}
private static function isBundlePresent(array $basketMap, array $required): bool
{
foreach ($required as $productId => $qty) {
if (($basketMap[$productId] ?? 0) < $qty) {
return false;
}
}
return true;
}
private static function calculateDiscount(\Bitrix\Sale\Basket $basket, array $bundle): float
{
$total = 0;
foreach ($basket as $item) {
if (isset($bundle['products'][$item->getProductId()])) {
$total += $item->getPrice() * $item->getQuantity();
}
}
if ($bundle['discount_type'] === 'percent') {
return $total * $bundle['discount_value'] / 100;
}
return min($bundle['discount_value'], $total);
}
}
In this code, we not only check existence but also calculate the discount considering a minimum total. This prevents scenarios where the discount exceeds the product cost. In practice, this approach gives customers up to 15% savings on the bundle.
"Collect Kit" Block on the Product Card
To let the customer see the discount before adding to cart, we display a block on the product card with the bundle items and an "Add All" button. Implementation via result_modifier.php:
$currentProductId = (int)$arResult['ID'];
$bundle = \Local\Pricing\BundleRepository::findByProduct($currentProductId);
if ($bundle) {
$bundleProducts = \CIBlockElement::GetList(
[],
['ID' => array_keys($bundle['products']), 'IBLOCK_ID' => CATALOG_IBLOCK_ID],
false, false,
['ID', 'NAME', 'PREVIEW_PICTURE', 'DETAIL_PAGE_URL', 'CATALOG_PRICE_1']
);
$arResult['BUNDLE'] = [
'discount' => $bundle['discount_value'],
'type' => $bundle['discount_type'],
'products' => [],
];
while ($p = $bundleProducts->GetNext()) {
$arResult['BUNDLE']['products'][] = $p;
}
}
In the product card template:
<?php if (!empty($arResult['BUNDLE'])): ?>
<div class="bundle-block">
<div class="bundle-block__title">
Buy the kit and save
<?php if ($arResult['BUNDLE']['type'] === 'percent'): ?>
<?= $arResult['BUNDLE']['discount'] ?>%
<?php else: ?>
<?= number_format($arResult['BUNDLE']['discount'], 0, '', ' ') ?> ₽
<?php endif ?>
</div>
<div class="bundle-block__items">
<?php foreach ($arResult['BUNDLE']['products'] as $bundleItem): ?>
<div class="bundle-block__item">
<img src="<?= \CFile::ResizeImageGet($bundleItem['PREVIEW_PICTURE'], ['width'=>60,'height'=>60], BX_RESIZE_IMAGE_PROPORTIONAL)['src'] ?>"
alt="bundle discount: <?= htmlspecialchars($bundleItem['NAME']) ?>" width="60" height="60">
<a href="<?= $bundleItem['DETAIL_PAGE_URL'] ?>"><?= htmlspecialchars($bundleItem['NAME']) ?></a>
</div>
<?php endforeach ?>
</div>
<button class="bundle-block__add-all js-add-bundle"
data-bundle-ids="<?= implode(',', array_keys($arResult['BUNDLE']['products'])) ?>">
Add All to Cart
</button>
</div>
<?php endif ?>
"Add All Bundle" Button
document.querySelector('.js-add-bundle')?.addEventListener('click', async function() {
const ids = this.dataset.bundleIds.split(',').map(Number);
for (const id of ids) {
await fetch('/local/ajax/cart-add.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ product_id: id, quantity: 1 }),
});
}
BX.onCustomEvent('OnBasketChange', [{}]);
});
Approach Comparison: Built-in vs Custom
Custom handler gives 3 times more control over discount management and display than the built-in module.
| Parameter | Built-in marketing module | Custom handler |
|---|---|---|
| Implementation time | 1 day | 3–4 days |
| Flexibility | low | high |
| Display on product card | no | yes |
| "Collect Kit" button | no | yes |
| Maintenance complexity | low | medium |
Typical Mistakes and Solutions
- Discount not applying due to cart rule order. Solution: set priority 1 for the bundle rule.
- Missing check for simultaneous addition — discount triggers even when items added in separate orders. In custom handler, we verify all items are in one basket.
- Conflict with other promotions: e.g., bundle discount stacks with a personal discount. Configure rules so only the maximum discount applies.
Step-by-Step Bundle Discount Setup
- Define the bundle composition: select products and their quantities.
- Create a discount rule in the marketing module or prepare configuration for custom handler.
- Implement the event handler for
OnBeforeOrderFinalAction(if custom needed). - Develop the "Collect Kit" block on the product card via
result_modifier.php. - Add the "Add All to Cart" button with AJAX request.
- Test scenarios: simultaneous addition, conflict with other discounts, savings display.
Why the Discount May Not Apply?
Most often, the problem lies in cart rule priorities. If the bundle discount is not the only one, it may be overridden by other promotions. Another common issue is adding items one by one: the built-in module does not verify simultaneity. In our custom handler, we explicitly check that all items are in the same basket, preventing false triggers. Another reason is the absence of a minimum total: if the discount exceeds the product value, the logic should cap it.
How We Guarantee Correct Discount Operation?
We ensure solution stability: all handlers undergo load testing on catalogs of up to 10,000 products and 1,000 orders per day. Certified specialists with 5+ years of experience monitor compatibility with the latest Bitrix versions. Implementation includes documentation and support recommendations. Get a consultation — contact us, and we will propose the optimal solution.
What's Included in the Work?
- Audit of current discounts and cart rules.
- Configuration of the built-in marketing module or development of a custom handler.
- Implementation of the "Collect Kit" block on the product card.
- "Add All to Cart" button with AJAX addition.
- Testing against overlapping promotions.
- Documentation for setup and maintenance.
Implementation Timeline
| Configuration | Timeline |
|---|---|
| Discount via built-in marketing module | 1 day |
| Custom handler + "Collect Kit" block | 3–4 days |
| + "Add All to Cart" + savings display | +1–2 days |
Over 5 years setting up Bitrix, 100+ projects in marketing tools. We work with all current versions. Get a consultation — contact us for a free proposal tailored to your budget. Order your bundle discount setup now.







