Сниппеты
Назад к сниппетам

Битрикс: поиск правила скидки с fallback по имени и значению

Метод resolveDiscount() для класса BirthdayCouponSender: поиск активного правила скидки сначала по фиксированному ID, затем по точному совпадению процента и USE_COUPONS, затем по ключевым словам в NAME/XML_ID.

php
27 июня 2026 г.
databasephpbitrix1c-bitrix
<?php

use Bitrix\Sale\Internals\DiscountTable;

// Константы класса BirthdayCouponSender
private const DISCOUNT_ID = 125;
private const DISCOUNT_PERCENT = 5.0;
private const DISCOUNT_NAME_HINTS = [
    'день рождения',
    'деньрождения',
    'birthday',
    'birth day',
    'др',
];
private static string $lastResolveDiscountError = '';

private static function resolveDiscount(): ?array
{
    self::$lastResolveDiscountError = '';

    $fixedDiscount = DiscountTable::getList([
        'select' => ['ID', 'NAME', 'XML_ID', 'LID', 'DISCOUNT_VALUE', 'DISCOUNT_TYPE', 'SORT', 'ACTIVE', 'USE_COUPONS'],
        'filter' => [
            '=ID' => self::DISCOUNT_ID,
            '=LID' => SITE_ID,
        ],
        'limit' => 1,
    ])->fetch();

    if (is_array($fixedDiscount)) {
        if ((string)($fixedDiscount['ACTIVE'] ?? 'N') !== 'Y') {
            self::$lastResolveDiscountError = 'Правило скидки ID=125 найдено, но оно неактивно.';
            self::log('skip', [
                'reason' => 'birthday discount exists but is inactive',
                'discount_id' => self::DISCOUNT_ID,
            ]);

            return null;
        }

        if ((string)($fixedDiscount['USE_COUPONS'] ?? 'N') !== 'Y') {
            self::$lastResolveDiscountError = 'Правило скидки ID=125 найдено, но в нем отключены купоны.';
            self::log('skip', [
                'reason' => 'birthday discount exists but coupons are disabled',
                'discount_id' => self::DISCOUNT_ID,
            ]);

            return null;
        }

        return $fixedDiscount;
    }

    $exactCandidates = self::loadDiscountCandidates([
        '=ACTIVE' => 'Y',
        '=USE_COUPONS' => 'Y',
        '=LID' => SITE_ID,
        '=DISCOUNT_TYPE' => 'P',
        '=DISCOUNT_VALUE' => self::DISCOUNT_PERCENT,
    ]);

    $discount = self::pickBestDiscount($exactCandidates);
    if ($discount !== null) {
        return $discount;
    }

    $fallbackCandidates = self::loadDiscountCandidates([
        '=ACTIVE' => 'Y',
        '=USE_COUPONS' => 'Y',
        '=LID' => SITE_ID,
    ]);

    $discount = self::pickBestDiscount($fallbackCandidates);
    if ($discount !== null) {
        return $discount;
    }

    self::$lastResolveDiscountError = 'Правило скидки ID=125 не найдено для сайта или недоступно в текущей конфигурации.';

    return null;
}

private static function loadDiscountCandidates(array $filter): array
{
    return DiscountTable::getList([
        'select' => ['ID', 'NAME', 'XML_ID', 'LID', 'DISCOUNT_VALUE', 'DISCOUNT_TYPE', 'SORT'],
        'filter' => $filter,
        'order' => ['SORT' => 'ASC', 'ID' => 'ASC'],
    ])->fetchAll();
}

private static function pickBestDiscount(array $discounts): ?array
{
    $bestDiscount = null;
    $bestScore = 0;

    foreach ($discounts as $discount) {
        $haystack = mb_strtolower(
            trim((string)($discount['NAME'] ?? '')) . ' ' . trim((string)($discount['XML_ID'] ?? ''))
        );
        $score = 0;

        foreach (self::DISCOUNT_NAME_HINTS as $hint) {
            if ($hint !== '' && mb_stripos($haystack, $hint) !== false) {
                $score += 100;
            }
        }

        if ((string)($discount['DISCOUNT_TYPE'] ?? '') === 'P'
            && (float)($discount['DISCOUNT_VALUE'] ?? 0) === self::DISCOUNT_PERCENT) {
            $score += 20;
        }

        if ($score > $bestScore) {
            $bestScore = $score;
            $bestDiscount = $discount;
        }
    }

    return $bestDiscount;
}

Как работает трёхуровневый fallback при поиске скидки

На первом уровне resolveDiscount() выполняет прямой запрос к DiscountTable по константе DISCOUNT_ID = 125. Это самый быстрый путь — один запрос с фильтром по ID и SITE_ID. Если правило найдено, дополнительно проверяется его активность (ACTIVE = 'Y') и наличие купонов (USE_COUPONS = 'Y'). Если хотя бы одно условие нарушено, метод возвращает null с информативным сообщением в $lastResolveDiscountError — это позволяет вызывающему коду показать точную причину отказа.

На втором уровне, если правило с фиксированным ID не найдено на текущем сайте (например, после переноса базы или пересоздания правила), поиск расширяется до всех активных правил с купонами, у которых тип скидки — процентный (DISCOUNT_TYPE = 'P') и значение равно DISCOUNT_PERCENT. Среди результатов метод pickBestDiscount() выбирает кандидата с наибольшим числом совпадений по ключевым словам в полях NAME и XML_ID. Каждое вхождение хинта из DISCOUNT_NAME_HINTS добавляет 100 очков, совпадение процентного значения — ещё 20.

На третьем уровне фильтр снимается до всех активных правил с купонами без ограничения по значению скидки. Этот уровень работает исключительно через ключевые слова. Если ни один уровень не дал результата, $lastResolveDiscountError содержит итоговую диагностическую строку, которая записывается в лог.

Где применять этот подход

Трёхуровневый поиск особенно полезен в модулях для нескольких клиентов, где ID правил скидок различаются на каждом проекте. Метод pickBestDiscount() можно переиспользовать для поиска других типов правил: акционных скидок, накопительных программ, скидок для групп пользователей. Достаточно передать другой набор хинтов и другое значение DISCOUNT_PERCENT.

resolveDiscount: поиск правила скидки в Битрикс Sale | Viku-Lov Studio