Налаштування перевірки замовлень на фрод 1С-Бітрікс

Наша компанія займається розробкою, підтримкою та обслуговуванням рішень на Бітрікс та Бітрікс24 будь-якої складності. Від простих односторінкових сайтів до складних інтернет-магазинів, CRM систем з інтеграцією 1С та телефонії. Досвід розробників підтверджено сертифікатами від вендора.
Пропоновані послуги
Показано 1 з 1 послугУсі 1626 послуг
Налаштування перевірки замовлень на фрод 1С-Бітрікс
Проста
~1 робочий день
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Розробка на базі Бітрікс, Бітрікс24, 1С для компанії Development of an Online
    585
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Розробка на базі 1С Підприємство для компанії МИРСАНБЕЛ
    751
  • image_crm_dolbimby_434_0.webp
    Розробка сайту на CRM Бітрікс24 для компанії DOLBIMBY
    657
  • image_crm_technotorgcomplex_453_0.webp
    Розробка на базі Бітрікс24 для компанії ТЕХНОТОРГКОМПЛЕКС
    989

Налаштування перевірки замовлень на фрод 1С-Бітрікс

Фрод-перевірка — це скорингова система, яка оцінює кожне замовлення за набором сигналів і приймає рішення: пропустити, поставити на ручну перевірку або заблокувати. На відміну від простого rate limiting, скоринг враховує сукупність факторів — один із яких може бути в нормі, а три разом — червоний прапор.

Скорингова система

Кожен перевірочний фактор дає кількість очок ризику. Підсумковий бал визначає дію.

namespace Local\Fraud;

class FraudScorer
{
    // Пороги
    private const BLOCK_SCORE  = 70;
    private const REVIEW_SCORE = 40;

    public function score(\Bitrix\Sale\Order $order): ScoreResult
    {
        $signals = [];

        $ip    = $_SERVER['REMOTE_ADDR'] ?? '';
        $props = $order->getPropertyCollection();
        $email = $props->getItemByOrderPropertyCode('EMAIL')?->getValue() ?? '';
        $phone = $props->getItemByOrderPropertyCode('PHONE')?->getValue() ?? '';
        $name  = trim(
            ($props->getItemByOrderPropertyCode('NAME')?->getValue() ?? '') . ' ' .
            ($props->getItemByOrderPropertyCode('LAST_NAME')?->getValue() ?? '')
        );

        // IP-сигнали
        $signals['ip_orders_1h']    = $this->ipOrders($ip, 1)    * 8;  // макс ~80 при 10 замовленнях
        $signals['ip_orders_24h']   = $this->ipOrders($ip, 24)   * 2;  // макс ~40 при 20 замовленнях
        $signals['ip_in_stoplist']  = $this->isInStopList($ip)    ? 80 : 0;

        // Email-сигнали
        $signals['disposable_email']  = $this->isDisposableEmail($email) ? 35 : 0;
        $signals['no_email']          = empty($email) ? 25 : 0;
        $signals['email_orders_24h']  = $this->emailOrders($email, 24) * 5;

        // Телефонні сигнали
        $signals['invalid_phone']   = !$this->isValidPhone($phone) ? 20 : 0;

        // Сума та історія
        $signals['high_amount_new']  = $this->highAmountNewUser($order) ? 30 : 0;
        $signals['unusual_amount']   = $this->isUnusualAmount($order, (int)$order->getUserId()) ? 15 : 0;

        // Ім'я
        $signals['suspicious_name']  = $this->isSuspiciousName($name) ? 20 : 0;

        $total = min(100, array_sum($signals));

        return new ScoreResult(
            score:       $total,
            signals:     array_filter($signals),
            action:      match(true) {
                $total >= self::BLOCK_SCORE  => 'block',
                $total >= self::REVIEW_SCORE => 'review',
                default                      => 'allow',
            }
        );
    }

    private function ipOrders(string $ip, int $hours): int
    {
        $safe = \Bitrix\Main\Application::getConnection()->getSqlHelper()->forSql($ip);
        return (int)\Bitrix\Main\Application::getConnection()->query(
            "SELECT COUNT(*) cnt FROM b_sale_order
             WHERE CREATED_BY_IP = '{$safe}'
               AND DATE_INSERT   > DATE_SUB(NOW(), INTERVAL {$hours} HOUR)"
        )->fetch()['cnt'];
    }

    private function isInStopList(string $ip): bool
    {
        $safe = \Bitrix\Main\Application::getConnection()->getSqlHelper()->forSql($ip);
        return (bool)\Bitrix\Main\Application::getConnection()->query(
            "SELECT ID FROM b_stop_list WHERE IP_ADDR = '{$safe}' AND ACTIVE = 'Y' LIMIT 1"
        )->fetch();
    }

    private function emailOrders(string $email, int $hours): int
    {
        if (empty($email)) return 0;
        $safe = \Bitrix\Main\Application::getConnection()->getSqlHelper()->forSql($email);
        return (int)\Bitrix\Main\Application::getConnection()->query(
            "SELECT COUNT(*) cnt
             FROM b_sale_order_props_value pv
             JOIN b_sale_order_props p ON p.ID = pv.ORDER_PROPS_ID
             JOIN b_sale_order o       ON o.ID = pv.ORDER_ID
             WHERE p.CODE = 'EMAIL'
               AND pv.VALUE = '{$safe}'
               AND o.DATE_INSERT > DATE_SUB(NOW(), INTERVAL {$hours} HOUR)"
        )->fetch()['cnt'];
    }

    private function isDisposableEmail(string $email): bool
    {
        $domain  = strtolower(substr(strrchr($email, '@'), 1));
        return in_array($domain, [
            'mailinator.com', 'guerrillamail.com', 'tempmail.com',
            'throwam.com', 'yopmail.com', '10minutemail.com',
        ], true);
    }

    private function isValidPhone(string $phone): bool
    {
        $digits = preg_replace('/\D/', '', $phone);
        return strlen($digits) >= 10 && strlen($digits) <= 15;
    }

    private function highAmountNewUser(\Bitrix\Sale\Order $order): bool
    {
        $userId = (int)$order->getUserId();
        if ($order->getPrice() < 30000 || $userId <= 0) return false;

        $prevCount = (int)\Bitrix\Main\Application::getConnection()->query(
            "SELECT COUNT(*) cnt FROM b_sale_order WHERE USER_ID = {$userId}"
        )->fetch()['cnt'];

        return $prevCount === 0;
    }

    private function isUnusualAmount(\Bitrix\Sale\Order $order, int $userId): bool
    {
        if ($userId <= 0) return false;

        $avg = (float)\Bitrix\Main\Application::getConnection()->query(
            "SELECT AVG(PRICE) avg FROM b_sale_order
             WHERE USER_ID = {$userId} AND STATUS_ID NOT IN ('C')"
        )->fetch()['avg'];

        return $avg > 0 && $order->getPrice() > $avg * 5;
    }

    private function isSuspiciousName(string $name): bool
    {
        // Повністю числове ім'я, занадто коротке, лише спецсимволи
        return preg_match('/^\d+$/', $name)
            || mb_strlen($name) < 3
            || preg_match('/[<>{}\\\\]/', $name);
    }
}

Результат перевірки

namespace Local\Fraud;

class ScoreResult
{
    public function __construct(
        public readonly int    $score,
        public readonly array  $signals,
        public readonly string $action,  // 'allow', 'review', 'block'
    ) {}

    public function isBlocked(): bool { return $this->action === 'block'; }
    public function needsReview(): bool { return $this->action === 'review'; }

    public function getComment(): string
    {
        $parts = ["[FRAUD_SCORE:{$this->score}]"];
        foreach ($this->signals as $signal => $value) {
            $parts[] = "{$signal}:{$value}";
        }
        return implode(' ', $parts);
    }
}

Логування результатів скорингу

Всі перевірки логуються в HL-блок FraudLog для аналізу та налаштування порогів:

Поле Значення
UF_ORDER_ID ID замовлення (якщо створено)
UF_IP IP-адреса
UF_EMAIL Email із замовлення
UF_SCORE Підсумковий бал
UF_ACTION allow / review / block
UF_SIGNALS JSON із деталізацією сигналів
UF_DATE Дата перевірки

Аналіз логів за 2–4 тижні дозволяє відкалібрувати пороги під конкретний магазин.

Терміни реалізації

Конфігурація Термін
Скорингова система з базовими сигналами 3–4 дні
+ логування, адміністративний інтерфейс +2 дні
+ калібрування на історичних даних +1 тиждень