Setting up a price reduction subscription for 1C-Bitrix products

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
    1173
  • 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
    745
  • 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

Price Drop Subscription Configuration in 1C-Bitrix

A customer looks at an expensive product and doesn't buy. A "Notify me when the price drops" button gives you a chance to bring them back at exactly the moment the price becomes acceptable. This works better than any mass mailing, because the user has already expressed interest in a specific product.

Data structure

HL block PriceSubscription for storing subscriptions:

b_uts_price_subscription
├── ID
├── UF_USER_ID      — user ID (0 = guest)
├── UF_EMAIL        — notification email
├── UF_PRODUCT_ID   — product ID (b_iblock_element.ID)
├── UF_TARGET_PRICE — desired price (0 = any drop)
├── UF_CURRENT_PRICE — price at the time of subscription
├── UF_ACTIVE       — whether the subscription is active
├── UF_NOTIFIED     — whether a notification was sent
└── UF_DATE_CREATE  — creation date

Subscription form

On the product card, the button appears next to the price:

// In the card template, below the price block
<?php if (!$arResult['CATALOG_ITEM']['CAN_BUY']): // only if the item cannot be purchased now ?>
<?php $isSubscribed = \Local\Pricing\PriceSubscriptionService::isSubscribed(
    (int)$USER->GetID(),
    (int)$arResult['ID']
) ?>
<button class="btn-price-subscribe js-price-subscribe
               <?= $isSubscribed ? 'is-active' : '' ?>"
        data-product-id="<?= $arResult['ID'] ?>"
        data-current-price="<?= $arResult['CATALOG_PRICE']['PRICE'] ?>">
    <?= $isSubscribed ? 'Subscription active' : 'Notify me when price drops' ?>
</button>
<?php else: ?>

For authenticated users — AJAX subscription. For guests — display a modal with an email field:

document.querySelectorAll('.js-price-subscribe').forEach(btn => {
    btn.addEventListener('click', async () => {
        const productId    = btn.dataset.productId;
        const currentPrice = btn.dataset.currentPrice;
        const email        = window.__userEmail || null; // passed from PHP

        if (!email) {
            // Show modal with email field
            showPriceSubscribeModal(productId, currentPrice);
            return;
        }

        const res = await fetch('/local/ajax/price-subscribe.php', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ product_id: productId, email, current_price: currentPrice }),
        }).then(r => r.json());

        if (res.success) {
            btn.classList.add('is-active');
            btn.textContent = 'Subscription active';
        }
    });
});

AJAX subscription handler

// /local/ajax/price-subscribe.php
\Bitrix\Main\Application::getInstance()->initializeExtended();

global $USER;
$data = json_decode(file_get_contents('php://input'), true);

$productId    = (int)($data['product_id'] ?? 0);
$email        = filter_var($data['email'] ?? '', FILTER_VALIDATE_EMAIL);
$currentPrice = (float)($data['current_price'] ?? 0);

if (!$productId || !$email) {
    echo json_encode(['success' => false, 'error' => 'Invalid data']);
    exit;
}

$result = \Local\Pricing\PriceSubscriptionService::subscribe(
    userId:       (int)$USER->GetID(),
    email:        $email,
    productId:    $productId,
    currentPrice: $currentPrice,
    targetPrice:  (float)($data['target_price'] ?? 0),
);

echo json_encode(['success' => $result]);

Subscription management service

namespace Local\Pricing;

use Bitrix\Highloadblock\HighloadBlockTable;

class PriceSubscriptionService
{
    public static function subscribe(
        int    $userId,
        string $email,
        int    $productId,
        float  $currentPrice,
        float  $targetPrice = 0
    ): bool {
        // Check for duplicate
        if (self::isSubscribed($userId, $productId, $email)) {
            return true; // already subscribed
        }

        $dataClass = self::getDataClass();
        $result    = $dataClass::add([
            'UF_USER_ID'      => $userId,
            'UF_EMAIL'        => $email,
            'UF_PRODUCT_ID'   => $productId,
            'UF_TARGET_PRICE' => $targetPrice,
            'UF_CURRENT_PRICE' => $currentPrice,
            'UF_ACTIVE'       => true,
            'UF_NOTIFIED'     => false,
        ]);

        return $result->isSuccess();
    }

    public static function isSubscribed(int $userId, int $productId, string $email = ''): bool
    {
        $dataClass = self::getDataClass();
        $filter    = ['UF_PRODUCT_ID' => $productId, 'UF_ACTIVE' => true];

        if ($userId > 0) {
            $filter['UF_USER_ID'] = $userId;
        } elseif ($email) {
            $filter['UF_EMAIL'] = $email;
        }

        return (bool)$dataClass::getRow(['filter' => $filter, 'select' => ['ID']]);
    }
}

Price drop check agent

The agent runs once an hour, finds products with a dropped price, and sends notifications:

namespace Local\Pricing;

class PriceDropNotifierAgent
{
    public static function run(): string
    {
        $dataClass   = PriceSubscriptionService::getDataClass();
        $subscriptions = $dataClass::getList([
            'filter' => ['UF_ACTIVE' => true, 'UF_NOTIFIED' => false],
            'select' => ['ID', 'UF_EMAIL', 'UF_PRODUCT_ID', 'UF_CURRENT_PRICE', 'UF_TARGET_PRICE'],
        ]);

        while ($sub = $subscriptions->fetch()) {
            $currentPrice = self::getCurrentPrice((int)$sub['UF_PRODUCT_ID']);

            if ($currentPrice === null) continue;

            $priceDropped = $currentPrice < $sub['UF_CURRENT_PRICE'];
            $targetReached = $sub['UF_TARGET_PRICE'] > 0
                ? $currentPrice <= $sub['UF_TARGET_PRICE']
                : $priceDropped;

            if ($targetReached) {
                self::sendNotification($sub, $currentPrice);
                $dataClass::update($sub['ID'], [
                    'UF_NOTIFIED'     => true,
                    'UF_CURRENT_PRICE' => $currentPrice,
                ]);
            }
        }

        return '\Local\Pricing\PriceDropNotifierAgent::run();';
    }

    private static function getCurrentPrice(int $productId): ?float
    {
        $res = \CCatalogPrice::GetList(
            [],
            ['PRODUCT_ID' => $productId, 'CATALOG_GROUP_ID' => 1]
        )->Fetch();

        return $res ? (float)$res['PRICE'] : null;
    }

    private static function sendNotification(array $sub, float $newPrice): void
    {
        $product = \CIBlockElement::GetByID($sub['UF_PRODUCT_ID'])->GetNext();
        if (!$product) return;

        \CEvent::Send('PRICE_DROP_NOTIFICATION', SITE_ID, [
            'EMAIL'       => $sub['UF_EMAIL'],
            'PRODUCT_NAME' => $product['NAME'],
            'PRODUCT_URL'  => 'https://' . SITE_SERVER_NAME . $product['DETAIL_PAGE_URL'],
            'OLD_PRICE'    => number_format($sub['UF_CURRENT_PRICE'], 0, '', ' '),
            'NEW_PRICE'    => number_format($newPrice, 0, '', ' '),
            'SAVINGS'      => number_format($sub['UF_CURRENT_PRICE'] - $newPrice, 0, '', ' '),
        ]);
    }
}

Implementation timelines

Configuration Timeline
Subscription (button + AJAX + HL block) 2–3 days
+ price check agent + email notification +2 days
+ target price, subscription account section +2–3 days