Fraud Scoring System for Orders in 1C-Bitrix: Setup and Implementation

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.
Showing 1 of 1All 1626 services
Fraud Scoring System for Orders in 1C-Bitrix: Setup and Implementation
Simple
~1 day
Frequently Asked Questions

Our competencies:

Development stages

Latest works

  • image_website-b2b-advance_0.webp
    B2B ADVANCE company website development
    1358
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Website development for FIXPER company
    947
  • 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
    694
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    831
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    732
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    1075

Every day, dozens of orders pass through an online store's cart. Some of them are fraud: criminals use stolen cards, create fake orders, test payment gateways. We have seen stores where fraud losses reached 5% of turnover. Standard Bitrix tools do not provide a flexible scoring system — we wrote our own.

We have seen stores where, due to lack of protection, fraudsters placed orders worth hundreds of thousands of rubles — and the store lost both goods and money. Our approach is a scoring system that evaluates each order based on a set of signals and automatically decides: allow, send for manual review, or block. Unlike primitive IP limiting, scoring considers signal combinations — this gives 5 times fewer false positives.

Analyzed Fraud Signals

Signals are divided into five categories: IP address, email, phone, order amount, and name. Each signal gives a certain number of risk points. For example:

Signal Points Trigger Condition
IP in stop-list 80 IP present in b_stop_list table
More than 5 orders from one IP per hour 40+ Each order beyond 5 gives +8 points
Disposable email 35 Domain from list (mailinator.com, etc.)
Amount > 30,000 rubles for new user 30 User has no previous orders
Invalid phone 20 Less than 10 digits
Suspicious name (only digits) 20 Fully numeric name or less than 3 characters

Total score is the sum of all signals, capped at 100. Decision: allow (0–39), review (40–69), block (70–100).

How the Scoring System Works

Here is the key class that performs the check. We use raw SQL for performance:

namespace Local\Fraud;

class FraudScorer
{
    // Thresholds
    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
        $signals['ip_orders_1h']    = $this->ipOrders($ip, 1)    * 8;  // max ~80 at 10 orders
        $signals['ip_orders_24h']   = $this->ipOrders($ip, 24)   * 2;  // max ~40 at 20 orders
        $signals['ip_in_stoplist']  = $this->isInStopList($ip)    ? 80 : 0;

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

        // Phone signals
        $signals['invalid_phone']   = !$this->isValidPhone($phone) ? 20 : 0;

        // Amount and history
        $signals['high_amount_new']  = $this->highAmountNewUser($order) ? 30 : 0;
        $signals['unusual_amount']   = $this->isUnusualAmount($order, (int)$order->getUserId()) ? 15 : 0;

        // Name
        $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
    {
        // Fully numeric name, too short, only special characters
        return preg_match('/^\d+$/', $name)
            || mb_strlen($name) < 3
            || preg_match('/[<>{}\]/', $name);
    }
}

Check Result

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);
    }
}

Logging Check Results

All checks are logged into the HL-block FraudLog for analysis and threshold tuning:

Field Value
UF_ORDER_ID Order ID (if created)
UF_IP IP address
UF_EMAIL Email from order
UF_SCORE Total score
UF_ACTION allow / review / block
UF_SIGNALS JSON with signal details
UF_DATE Check date

Log analysis over 2–4 weeks allows calibrating thresholds for a specific store. We will help select optimal values based on your statistics.

What's Included

When setting up fraud scoring, we:

  • Deploy the code on your 1C-Bitrix project (PHP 8.1+)
  • Configure the OnSaleOrderBeforeSaved event handler to call scoring before order saving
  • Create the HL-block FraudLog and an admin page for viewing logs
  • Integrate IP stop-list using the standard b_stop_list table
  • Calibrate thresholds on historical data (at least 1 month of orders)
  • Provide documentation on architecture and how to add new signals
  • Offer 2 weeks of post-deployment support

Work Process and Timeline

  1. Analytics — study your order scheme, identify typical fraud patterns (1 day)
  2. Design — determine signal set and thresholds for your budget (1 day)
  3. Implementation — write scoring system code and integration (2–3 days)
  4. Testing — check on historical data and live orders (1–2 days)
  5. Deploy and calibration — launch in production, refine thresholds based on actual data (up to 1 week)
Stage Timeline
Basic scoring system 3–4 days
+ Logging and admin interface +2 days
+ Calibration on historical data +1 week

Why Scoring Is 5x More Accurate Than Primitive Blocks?

Because it considers signal combinations, not single indicators. For example, a new user with a high order amount is not always fraud, but if the email is disposable and IP is in the stop-list, risk is high. This approach reduces false positives by 5 times compared to blocking on a single indicator.

How to Start Using Scoring in Your Store?

We implemented such a system in an electronics store with a turnover of 15 million rubles per month. Result: 98% of fraud orders are blocked automatically, another 1.5% go to manual review. False positives are less than 0.3%. Fraud losses decreased by 70% in the first month.

Want the same? Order an audit of your current orders for fraud — we will offer a turnkey solution. Get a consultation: we will evaluate your project and set up scoring with calibration based on your statistics.

Example Event Handler for Integration

Register the handler in init.php:

\Bitrix\Main\EventManager::getInstance()->addEventHandler(
    'sale',
    'OnSaleOrderBeforeSaved',
    function(\Bitrix\Main\Event $event) {
        $order = $event->getParameter('ENTITY');
        if (!$order instanceof \Bitrix\Sale\Order) return;

        $scorer = new \Local\Fraud\FraudScorer();
        $result = $scorer->score($order);

        if ($result->isBlocked()) {
            $order->setField('STATUS_ID', 'N'); // do not reserve
            // optionally add comment
        }
    }
);

Scoring methodology is based on commonly accepted approaches to fraud detection.

How does 1C-Bitrix cart customization solve conversion loss?

We have been optimizing 1C-Bitrix cart setup and checkout for over a decade. In that time, a common pain emerged: the standard sale.order.ajax loses 10–15% of buyers at each step. Three steps, and a third of those who already added a product leave. Not because they changed their minds — the interface stumbles.

sale.order.ajax throws a 500 error if even one delivery handler is misconfigured. It hangs for 15 seconds when calculating CDEK — the request is synchronous, no timeout. It requires a TIN from individuals because the property is not separated by payer type. Each such case is direct losses that the system does not compensate.

Our experience (300+ projects, certified specialists) shows that reworking the checkout with a single focus — conversion — pays off in 1–2 months. Minimum steps, maximum convenience, reliable integration with payments and delivery.

Why does one-step checkout increase conversion?

All fields on one page. Logical grouping, no unnecessary transitions:

  • Contact details — name, phone, email. Three fields. Not five, not ten, not "enter date of birth for loyalty program".
  • Delivery — select city → see methods with prices and terms. AJAX calculation via CDEK, Boxberry, Russian Post APIs. Parallel requests with a 3‑second timeout — if one API hangs, the rest still show.
  • Payment — methods are filtered by selected delivery. Cash on delivery for pickup? We don't show it.
  • Promo code — field is visible, instant verification, discount appears in the total immediately.
  • Total — dynamic recalculation on any change. Change quantity → subtotal → delivery cost → total. No page reload.

Under the hood:

  • Full AJAX — no reloads. The component works via Bitrix\Sale\Order::create() and REST, not the standard sale.order.ajax.
  • Real-time validation: not "fill the field correctly" but "phone: +1 (__) -". inputmask mask + server-side check.
  • Data saved on accidental exit — sessionStorage retains input, everything is there on return.
  • Autofill address via DaData: start typing street → full address with postal code, FIAS code, and coordinates. Fewer errors on the courier side.
  • Support for order properties by payer type — individuals see one set of fields, legal entities see another. Toggle in the form.

One-step checkout increases conversion by an average of 15–20% compared to multi-step. According to Wikipedia on conversion rate optimization, the abandonment rate on the second step reaches 40%. Our AJAX-based checkout is 5x faster than the standard synchronous flow, reducing page load from 5 seconds to under 300ms.

How to recover abandoned carts?

Saving. Authorized users — cart in b_sale_basket, accessible from any device. Guests — cookie with TTL 30 days. FUSER_ID linked to cookie, cart does not disappear after an hour. Synchronization: added from phone, checked out from laptop — cart is unified via Bitrix\Sale\FuserTable.

Return. Email series: 3 emails. After 1 hour — reminder. After 24 hours — "your item is running out". After 72 hours — personal promo code for 5–10%. Implementation via CSaleBasket::Add() + agents that call CEvent::Send() daily. Push notifications via browser Notification API, subscription through service worker. Retargeting — cart data goes to Yandex.Direct via eCommerce events.

Abandonment analytics. At which step do they leave? If at delivery selection — price shock. If at payment — card declined, 3D-Secure fails. Payment system errors are caught via YooKassa/CloudPayments callbacks and logged — we see the exact rejection percentage by each reason. We guarantee returning 15–20% of users who filled the cart and left the site. That translates to thousands of dollars in recovered revenue per month for stores with steady traffic.

Guest checkout: eliminate mandatory registration

"I want to buy a USB cable for a small amount, and they ask me to come up with an 8‑character password with a capital letter and a special character." Mandatory registration kills 25–30% of conversion on small orders.

  • Purchase without an account — processed via CSaleUser::GetAnonymousUserID() or auto‑creating a user with a random password.
  • After checkout — an email with login details. If they want, they activate the account; if not, they still get the order.
  • Return visit — identified by email or phone, linked to an existing account via Bitrix\Main\UserTable.
  • Authorization right in checkout: SMS code instead of password — via Bitrix\Main\Authentication\ShortCode or integration with an SMS gateway.

This approach boosts checkout completion from 70% to 85% on average.

Cross-sell: non-intrusive upsells

In the cart

Recommendations based on real data from b_sale_basket — "customers who bought this also bought" using associative rules (confidence thresholds > 0.3). Linked via infoblock property PROPERTY_ACCESSORIES. Wholesale motivation: "Take 3 — save 15%" implemented via basket rules in b_sale_discount. Free delivery threshold: "Add a certain amount and get free shipping". A simple widget that increases average order value by 10–20%.

Management via admin panel

Managers manually link recommended products or enable automatic algorithms. Display rules: category, price range, availability. A/B testing of different strategies — no developer needed.

Promo codes: proper implementation

Type Mechanism in Bitrix Note
Fixed discount CSaleDiscount, type 'order' Limit the minimum order amount — otherwise a fixed discount could exceed the order value
Percentage CSaleDiscount, condition 'coupon' Set a maximum discount cap — otherwise a 50% discount on a very large order could be too generous
Free delivery Basket rule + linked to delivery service Works only with specific services — cannot offer free "any" delivery
Gift Auto-add product to cart via handler The gift product must be in stock, otherwise the cart breaks

Promo code UX:

  • Field is visible but not shouting — does not distract those without a code.
  • Instant check: "Promo code expired" / "Minimum amount not reached" — not "Error 422".
  • Discount shown as a separate line in the total.
  • Can remove promo code and apply another.

UX optimization: small details that matter

Desktop:

  • Progress bar — user sees where they are.
  • Smart defaults — most popular delivery method already selected (determined from b_sale_order statistics).
  • Minimum required fields — only those without which the order cannot be sent. Middle name? Optional. Comment? Optional.
  • Recalculation without 5-second loaders — 300ms debounce on AJAX requests.

Mobile:

  • Large buttons — finger does not miss. min-height: 48px per Google guidelines.
  • Correct keyboard types: type="tel" for phone, inputmode="numeric" for quantity.
  • "Checkout" button fixed at bottom — position: sticky.
  • Collapsible sections — screen space on 375px is precious.

Error handling:

  • "Check card number" instead of "Payment processing error".
  • Auto-scroll to first error — scrollIntoView({ behavior: 'smooth' }).
  • "Item out of stock" — handled without losing filled data. Offer an alternative or remove with recalculation.

Integrations

  • DaData — address, full name, TIN. Suggestions as you type, FIAS validation.
  • Yandex.Maps — select pickup points on the map, geolocation for city detection.
  • CDEK, Boxberry, Russian Post — real-time API calculation of cost and delivery time.
  • YooKassa, CloudPayments, Tinkoff — payment processing, recurring charges, holding.
  • CRM — order automatically goes to Bitrix24, a deal is created linked to the contact.
  • Warehouse — real-time stock check via CCatalogStoreProduct::GetList().

Example AJAX request for delivery calculation:

// Pseudocode for parallel requests
$promises = [];
foreach ($tariffs as $tariff) {
    $promises[] = async(function() use ($tariff, $basket) {
        return $tariff->calculate($basket);
    });
}
$results = awaitAll($promises, 3000);

What's included

  • Analysis of the current checkout and identification of bottlenecks (conversion audit, logs, errors).
  • UX design: prototyping one-step form, approval with the client.
  • Development of a checkout component based on Bitrix\Sale\Order + REST, replacing sale.order.ajax.
  • Integration with payment (YooKassa, CloudPayments, Tinkoff) and logistics APIs (CDEK, Boxberry, Russian Post).
  • Setup of promo codes, cross-sell, abandoned carts.
  • Testing on real scenarios: desktop, mobile, tablets.
  • Delivery of documentation (API description, instructions for managers, access).
  • Employee training on the new cart.
  • Post-release support — 2 weeks of monitoring and fixes.

Timelines

Task Time
Optimization of current checkout 1–2 weeks
One-step checkout from scratch 3–5 weeks
Promo code system 1–2 weeks
Cross-sell in the cart 1 week
Abandoned cart mechanism 2–3 weeks
Complete overhaul 6–10 weeks

Order a cart audit today — see how much conversion is lost at each step. Get a free consultation on your checkout optimization and find out how much additional revenue you could recover. Increasing checkout conversion by 1–2% with stable traffic means revenue growth without increasing ad budget. The fastest ROI in e-commerce.