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

Битрикс: защита от дублей купонов через маркер в поле DESCRIPTION

Методы hasCouponForYear() и buildCouponMarker() для класса BirthdayCouponSender: проверка уже выданных купонов за текущий год через строку-маркер 'birthday_coupon:user=N;year=YYYY' в поле DESCRIPTION таблицы купонов.

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

use Bitrix\Main\Type\DateTime;
use Bitrix\Sale\Internals\DiscountCouponTable;

// Константы класса BirthdayCouponSender
private const COUPON_VALID_DAYS_AFTER_BIRTHDAY = 5;

private static function hasCouponForYear(int $userId, int $year): bool
{
    $existingCoupon = DiscountCouponTable::getList([
        'select' => ['ID'],
        'filter' => [
            '=USER_ID' => $userId,
            '=DESCRIPTION' => self::buildCouponMarker($userId, $year),
        ],
        'limit' => 1,
    ])->fetch();

    return is_array($existingCoupon);
}

private static function buildCouponMarker(int $userId, int $year): string
{
    return 'birthday_coupon:user=' . $userId . ';year=' . $year;
}

private static function createCoupon(int $userId, array $discount, \DateTimeImmutable $birthdayDate): ?string
{
    $coupon = DiscountCouponTable::generateCoupon(true);
    $today = new DateTime();
    $activeTo = DateTime::createFromPhp(
        \DateTime::createFromImmutable(
            $birthdayDate->modify('+' . self::COUPON_VALID_DAYS_AFTER_BIRTHDAY . ' days')->setTime(23, 59, 59)
        )
    );

    $result = DiscountCouponTable::add([
        'DISCOUNT_ID' => (int)$discount['ID'],
        'ACTIVE' => 'Y',
        'ACTIVE_FROM' => clone $today,
        'ACTIVE_TO' => $activeTo,
        'COUPON' => $coupon,
        'TYPE' => DiscountCouponTable::TYPE_ONE_ORDER,
        'MAX_USE' => 1,
        'USER_ID' => $userId,
        'DESCRIPTION' => self::buildCouponMarker($userId, (int)$birthdayDate->format('Y')),
    ]);

    if (!$result->isSuccess()) {
        self::log('error', [
            'user_id' => $userId,
            'discount_id' => (int)$discount['ID'],
            'message' => implode('; ', $result->getErrorMessages()),
        ]);

        return null;
    }

    return $coupon;
}

Почему DESCRIPTION, а не отдельная таблица для маркировки купонов

Поле DESCRIPTION в таблице купонов Битрикс (b_sale_discount_coupon) — это текстовое поле, предназначенное для свободных заметок администратора. В D7 ORM оно полностью доступно через DiscountCouponTable как в записи, так и в фильтрации. Строка birthday_coupon:user=42;year=2026 достаточно специфична, чтобы не пересечься с обычными административными комментариями, и одновременно читаема: любой разработчик или администратор сразу поймёт, что означает эта запись.

Создание отдельной таблицы для хранения маркеров выданных купонов потребовало бы: написания ORM-класса, добавления миграции (или ручного CREATE TABLE при деплое), логики очистки устаревших записей (иначе таблица растёт бесконечно), и отдельного запроса к этой таблице вместо одного запроса к уже существующей таблице купонов. Все эти затраты не оправданы: таблица b_sale_discount_coupon уже содержит USER_ID и DESCRIPTION, индексируется по USER_ID, и фильтр по точному совпадению строки в DESCRIPTION при наличии фильтра по USER_ID работает достаточно быстро даже на больших таблицах. Оператор = (точное равенство) в D7 ORM транслируется в SQL = 'birthday_coupon:user=42;year=2026', а не в LIKE '%...%', что исключает полное сканирование.