Setting up automatic price adjustments for competitors 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
    1177
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    811
  • 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
    564
  • 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
    655
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    976

Configuring Automatic Price Adjustment by Competitors in 1C-Bitrix

Automatic price adjustment means the system updates product prices in response to competitor price changes, without any manager intervention. It sounds straightforward, but an implementation without restriction rules triggers price wars: your store drops its price, the competitor responds, you drop it further. Within a day, both sides are selling at a loss. This is why auto-adjustment is not simply "watch the competitor and copy" — it is a rule system with ceilings, floors, and priority logic.

Repricing Rule Model

Rules are stored in bl_repricing_rules:

CREATE TABLE bl_repricing_rules (
    id              SERIAL PRIMARY KEY,
    name            VARCHAR(255) NOT NULL,
    scope_type      VARCHAR(20) NOT NULL,       -- 'product', 'section', 'all'
    scope_id        INT,                        -- product or section ID
    strategy        VARCHAR(30) NOT NULL,       -- 'beat_min', 'match_min', 'avg', 'position'
    value           NUMERIC(8,4),               -- for beat_min: -50 (currency) or -0.05 (5%)
    value_type      VARCHAR(10) DEFAULT 'abs',  -- 'abs' | 'pct'
    floor_type      VARCHAR(10) DEFAULT 'margin', -- 'margin' | 'abs' | 'cost_pct'
    floor_value     NUMERIC(8,4),               -- minimum margin or absolute threshold
    ceiling_type    VARCHAR(10) DEFAULT 'abs',
    ceiling_value   NUMERIC(12,2),              -- upper price limit
    competitor_ids  INT[],                      -- NULL = all competitors
    priority        SMALLINT DEFAULT 10,
    active          BOOLEAN DEFAULT true
);

Strategies:

  • beat_min — stay X currency units/% below the minimum competitor price
  • match_min — match the minimum competitor price
  • avg — maintain the arithmetic average
  • position — hold the Nth cheapest position

New Price Calculation Engine

class RepricingEngine
{
    public function calculate(int $productId): ?array
    {
        $rule = $this->getApplicableRule($productId);
        if (!$rule) return null;

        $competitorPrices = $this->getCompetitorPrices($productId, $rule['competitor_ids']);
        if (empty($competitorPrices)) return null;

        $targetPrice = match($rule['strategy']) {
            'beat_min'  => $this->beatMin($competitorPrices, $rule),
            'match_min' => min($competitorPrices),
            'avg'       => array_sum($competitorPrices) / count($competitorPrices),
            'position'  => $this->targetPosition($competitorPrices, $rule['value']),
            default     => null,
        };

        if ($targetPrice === null) return null;

        // Apply constraints
        $floor   = $this->calcFloor($productId, $rule);
        $ceiling = (float)$rule['ceiling_value'];

        $finalPrice = max($targetPrice, $floor);
        if ($ceiling > 0) $finalPrice = min($finalPrice, $ceiling);

        // Round to 0 or 9 in the cent position
        $finalPrice = $this->roundPrice($finalPrice);

        $currentPrice = $this->getCurrentPrice($productId);
        if (abs($finalPrice - $currentPrice) < 0.01) return null; // No change

        return [
            'product_id'    => $productId,
            'current_price' => $currentPrice,
            'new_price'     => $finalPrice,
            'rule_id'       => $rule['id'],
            'reason'        => $rule['strategy'],
            'competitor_min'=> min($competitorPrices),
        ];
    }

    private function beatMin(array $prices, array $rule): float
    {
        $min = min($prices);
        return $rule['value_type'] === 'pct'
            ? $min * (1 + $rule['value'] / 100)
            : $min + $rule['value'];
    }

    private function calcFloor(int $productId, array $rule): float
    {
        if ($rule['floor_type'] === 'margin') {
            $cost = $this->getCostPrice($productId);
            return $cost > 0 ? $cost * (1 + $rule['floor_value'] / 100) : 0;
        }
        return (float)$rule['floor_value'];
    }
}

Applying the Price in Bitrix

class RepricingApplicator
{
    public function apply(array $change): void
    {
        // Log BEFORE the change
        RepricingLogTable::add([
            'PRODUCT_ID'    => $change['product_id'],
            'RULE_ID'       => $change['rule_id'],
            'OLD_PRICE'     => $change['current_price'],
            'NEW_PRICE'     => $change['new_price'],
            'REASON'        => $change['reason'],
            'APPLIED_AT'    => new \Bitrix\Main\Type\DateTime(),
        ]);

        // Update price
        $priceResult = \CCatalogProduct::SetPrice(
            $change['product_id'],
            BASE_PRICE_TYPE_ID,
            $change['new_price'],
            'RUB'
        );

        if (!$priceResult) {
            // Rollback and error notification
            RepricingLogTable::update($logId, ['STATUS' => 'error']);
        }
    }
}

Automatic Repricing Agent

Runs once per hour. Retrieves the list of products whose competitor prices have been updated in the last hour, passes them through RepricingEngine, and either applies the changes or queues them for manual approval.

Configuration via module options: each catalog section has an individual mode (auto | manual | disabled). Only products in sections set to auto are changed automatically.

Price War Protection

  1. Cooldown: after a price change, the next change cannot occur sooner than N hours later
  2. Max change per day: no more than X% price movement within 24 hours
  3. Competitor verification: do not react if the competitor lowered their price less than 30 minutes ago (guards against flash promotions)
  4. Minimum margin lock: if the rule produces a price below the margin floor, log "cannot apply" and notify the manager

Timeline

Phase Duration
Rule model + database schema 2 days
Calculation engine (all strategies) 3 days
Application agent + logging 2 days
Admin interface for rules 2 days
Edge-case testing 2 days
Total 11–13 days