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
- Cooldown: after a price change, the next change cannot occur sooner than N hours later
- Max change per day: no more than X% price movement within 24 hours
- Competitor verification: do not react if the competitor lowered their price less than 30 minutes ago (guards against flash promotions)
- 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 |







