Setting up a unified online and offline loyalty program in 1C-Bitrix

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.
Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1212
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    815
  • 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
    565
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    747
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    657
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    980

Configuring Unified Loyalty Program Online and Offline 1С-Bitrix

Customer accumulated 500 bonus points on the site and came to the store to spend them. Cashier doesn't see the bonuses — they're in a different system. Or vice versa: purchase in the store doesn't credit online account. Gap between online and offline loyalty program is not only a UX problem, but also direct losses on repeat purchases.

How loyalty works in Bitrix

The sale module implements discount system via b_sale_user_discount (personal discounts) and bonus system via b_sale_discount (cart rules). For full-featured loyalty program (accumulative points), a separate module or external system integration is used.

Bitrix24 has CRM module with bonuses (b_crm_loyalty_bonus_transaction), but for online store without Bitrix24, the marketingcrm module or custom transactions table is more often applied.

Bonus storage structure

Minimal structure for unified program:

CREATE TABLE bl_loyalty_account (
    id          SERIAL PRIMARY KEY,
    user_id     INT UNIQUE,           -- b_user.ID (online)
    card_number VARCHAR(20) UNIQUE,   -- card number for offline
    balance     NUMERIC(10,2) DEFAULT 0,
    created_at  TIMESTAMP DEFAULT NOW()
);

CREATE TABLE bl_loyalty_transaction (
    id          SERIAL PRIMARY KEY,
    account_id  INT NOT NULL REFERENCES bl_loyalty_account(id),
    amount      NUMERIC(10,2) NOT NULL,  -- positive=earn, negative=spend
    type        VARCHAR(20) NOT NULL,    -- 'earn_online', 'earn_offline', 'spend', 'expire'
    order_id    INT,                     -- b_sale_order.ID or external offline receipt ID
    source      VARCHAR(20) NOT NULL,    -- 'web', 'pos', 'mobile'
    created_at  TIMESTAMP DEFAULT NOW()
);

Transactional model with history is the only reliable way to store bonuses. Never update balance directly without recording a transaction. balance is either denormalized aggregate (updated by DB trigger) or calculated as SUM(amount) from the transactions table.

Customer identification at offline cashbox

Key task: cashier must find customer account. Identification methods:

  • Loyalty card number (physical card or barcode in mobile app)
  • Phone number
  • QR-code with token (generated in personal account)

When identifying by phone, cashbox software sends request to Bitrix API:

// /local/ajax/loyalty/find-account.php
$phone     = normalizePhone($_POST['phone']);
$bitrixUser = \CUser::GetList([], ['PERSONAL_PHONE' => $phone])->Fetch();

if ($bitrixUser) {
    $account = getLoyaltyAccount($bitrixUser['ID']);
    echo json_encode(['balance' => $account['balance'], 'account_id' => $account['id']]);
}

Bonus accrual on online order

Event handler on status change — after delivery/completion:

AddEventHandler('sale', 'OnSaleStatusOrderChange', function(\Bitrix\Main\Event $event) {
    $order  = $event->getParameter('ENTITY');
    $status = $order->getField('STATUS_ID');

    if ($status !== 'F') return; // Only completed orders

    $userId = $order->getUserId();
    $bonus  = round($order->getPrice() * BONUS_RATE); // BONUS_RATE = 0.05 (5%)

    addLoyaltyTransaction($userId, $bonus, 'earn_online', $order->getId(), 'web');
});

Bonus deduction on online purchase

Bonuses are applied via cart rule or custom payment method. Cart rule (b_sale_discount) can give fixed-amount discount. For more flexible scheme — custom "payment method" like "Pay with Bonuses", which when completing the order deducts transaction from bl_loyalty_transaction.

Synchronization via API for offline cashbox

Offline cashbox calls three endpoints:

  1. GET /loyalty/balance?phone=... — check balance
  2. POST /loyalty/spend — deduct bonuses on sale (must be transactional: sale start → reservation → confirmation)
  3. POST /loyalty/earn — accrue bonuses after sale

Reservation on deduction is a critical step. Without it, two parallel requests from different cashboxes can simultaneously read balance of 500 bonuses and deduct 500 twice, going into negative. Reservation via SELECT ... FOR UPDATE in PostgreSQL or via strict UPDATE with result check:

UPDATE bl_loyalty_account
SET balance = balance - :amount
WHERE id = :account_id AND balance >= :amount
RETURNING balance;
-- If 0 rows updated — insufficient bonuses

What we configure

  • Tables bl_loyalty_account and bl_loyalty_transaction
  • API-endpoints for cashbox systems (balance, accrual, deduction)
  • Event handlers OnSaleStatusOrderChange for online accruals
  • Customer identification mechanism by phone/card
  • Transactional deduction with race condition protection
  • Administrative interface for transaction history viewing