Setting up a price reduction trigger for a viewed product in 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
    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

Configuring a Price-Drop Trigger for Viewed Products in 1C-Bitrix

A user has viewed a product several times without purchasing — a clear signal of interest. Automatically reducing the price 24–48 hours after the view, or issuing a personalised coupon, converts these users better than standard email campaigns. In Bitrix this is implemented via the marketing module and an event handler.

Tracking Views

Product views are written to an HL-block table. The standard statistics module (b_stat_page_event) is not suitable — we need a link between a specific user and a specific product:

// /local/lib/Tracking/ProductViewTracker.php
namespace Local\Tracking;

class ProductViewTracker
{
    public static function track(int $userId, int $productId): void
    {
        if ($userId <= 0) return; // authenticated users only

        $conn = \Bitrix\Main\Application::getConnection();

        // Upsert: update view counter and last-viewed date
        $conn->queryExecute("
            INSERT INTO b_uts_product_view
                (UF_USER_ID, UF_PRODUCT_ID, UF_VIEW_COUNT, UF_LAST_VIEW, UF_TRIGGER_SENT)
            VALUES
                ({$userId}, {$productId}, 1, NOW(), 'N')
            ON DUPLICATE KEY UPDATE
                UF_VIEW_COUNT = UF_VIEW_COUNT + 1,
                UF_LAST_VIEW  = NOW()
        ");
    }
}

Call this in the product card component, inside result_modifier.php:

// /local/templates/.default/components/bitrix/catalog.element/main/result_modifier.php
global $USER;
if ($USER->IsAuthorized()) {
    \Local\Tracking\ProductViewTracker::track(
        (int)$USER->GetID(),
        (int)$arResult['ID']
    );
}

Trigger Agent

The agent runs once an hour, finds users meeting the qualifying criteria, and applies a discount:

namespace Local\Marketing;

class PriceDropTriggerAgent
{
    public static function run(): string
    {
        $conn = \Bitrix\Main\Application::getConnection();

        // Products viewed 2+ times, not purchased, last viewed 24+ hours ago
        $candidates = $conn->query("
            SELECT pv.UF_USER_ID, pv.UF_PRODUCT_ID, pv.UF_VIEW_COUNT
            FROM b_uts_product_view pv
            LEFT JOIN b_sale_basket sb
                ON sb.USER_ID = pv.UF_USER_ID
                AND sb.PRODUCT_ID = pv.UF_PRODUCT_ID
                AND sb.ORDER_ID IS NOT NULL
            WHERE pv.UF_VIEW_COUNT  >= 2
              AND pv.UF_LAST_VIEW   < DATE_SUB(NOW(), INTERVAL 24 HOUR)
              AND pv.UF_TRIGGER_SENT = 'N'
              AND sb.ID IS NULL
            LIMIT 50
        ");

        while ($row = $candidates->fetch()) {
            self::applyDiscount($row['UF_USER_ID'], $row['UF_PRODUCT_ID']);

            // Mark to prevent re-application
            $conn->queryExecute("
                UPDATE b_uts_product_view
                SET UF_TRIGGER_SENT = 'Y'
                WHERE UF_USER_ID = {$row['UF_USER_ID']}
                  AND UF_PRODUCT_ID = {$row['UF_PRODUCT_ID']}
            ");
        }

        return '\Local\Marketing\PriceDropTriggerAgent::run();';
    }

    private static function applyDiscount(int $userId, int $productId): void
    {
        // Create a personalised coupon via the marketing module
        $couponCode = 'VIEW_' . strtoupper(substr(md5($userId . $productId . time()), 0, 8));

        \CCatalogDiscountCoupon::Add([
            'DISCOUNT_ID'    => VIEWED_PRODUCT_DISCOUNT_ID, // ID of the pre-created 10% discount
            'CODE'           => $couponCode,
            'ONE_TIME'       => 'Y', // single-use
            'ACTIVE'         => 'Y',
            'ACTIVE_FROM'    => new \Bitrix\Main\Type\DateTime(),
            'ACTIVE_TO'      => \Bitrix\Main\Type\DateTime::createFromTimestamp(time() + 86400 * 3),
            'MAX_USE'        => 1,
        ]);

        // Associate the coupon with the user via HL-block
        PersonalCouponRepository::save($userId, $productId, $couponCode);

        // Email the user
        self::sendEmail($userId, $productId, $couponCode);
    }
}

Discount via the Marketing Module

The base discount is created once through the admin interface (Marketing → Discounts) or via API:

\CCatalogDiscount::Add([
    'NAME'        => 'Personal discount on a viewed product',
    'LID'         => SITE_ID,
    'ACTIVE'      => 'Y',
    'VALUE'       => 10, // 10% discount
    'VALUE_TYPE'  => 'P',
    'COUPON_TYPE' => 'U', // coupon required
    'SORT'        => 300,
    'PRIORITY'    => 1,
]);

The ID of this discount is assigned to the constant VIEWED_PRODUCT_DISCOUNT_ID.

Data Retention

View records are purged by an agent once a week: records older than 30 days with the trigger already sent are deleted. The table does not grow to an unmanageable size.

Configuration Timeline
Tracking + agent + email + coupon 3–5 days
+ trigger conversion analytics +2 days