Битрикс: защита от дублей купонов через маркер в поле DESCRIPTION
Методы hasCouponForYear() и buildCouponMarker() для класса BirthdayCouponSender: проверка уже выданных купонов за текущий год через строку-маркер 'birthday_coupon:user=N;year=YYYY' в поле DESCRIPTION таблицы купонов.
<?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 '%...%', что исключает полное сканирование.