Setting up competitor price tracking for 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

Competitor Price Monitoring Setup for 1C-Bitrix

Competitor price tracking is built using one of two approaches: a ready-made monitoring service (Competera, Metacommerce, Priceva) or a custom parser. A ready-made service is simpler and more reliable, but expensive for a large catalogue. A custom parser is flexible and cheap to operate, but requires regular maintenance whenever competitor websites change. In both cases the task of setup is the same: collect data, store it in 1C-Bitrix, and present it to the manager in a convenient view.

Data Storage Architecture

Regardless of the data source (service or parser), the storage structure is the same:

Competitors bl_price_competitors:

CREATE TABLE bl_price_competitors (
    id       SERIAL PRIMARY KEY,
    name     VARCHAR(255) NOT NULL,
    domain   VARCHAR(255) UNIQUE,
    active   BOOLEAN DEFAULT true,
    logo_url VARCHAR(512)
);

Competitor Prices bl_competitor_prices:

CREATE TABLE bl_competitor_prices (
    id             SERIAL PRIMARY KEY,
    product_id     INT NOT NULL,               -- b_iblock_element.ID
    competitor_id  INT REFERENCES bl_price_competitors(id),
    price          NUMERIC(12,2) NOT NULL,
    url            VARCHAR(512),               -- competitor's page URL
    in_stock       BOOLEAN DEFAULT true,
    checked_at     TIMESTAMP NOT NULL DEFAULT NOW(),
    UNIQUE (product_id, competitor_id)         -- one current price
);
CREATE INDEX idx_comp_prices_product ON bl_competitor_prices(product_id, checked_at DESC);

History bl_competitor_prices_history — a table partitioned by month to store changes without bloating the main table.

Integration via Monitoring Service API

When using a ready-made service, an agent fetches data and writes it to bl_competitor_prices:

function SyncCompetitorPrices(): string
{
    $client = new PriceMonitoringClient(MONITORING_API_KEY);
    $data   = $client->getPrices(['date' => date('Y-m-d')]);

    foreach ($data['products'] as $item) {
        $productId = ProductMapper::findBySku($item['sku']);
        if (!$productId) continue;

        foreach ($item['competitors'] as $comp) {
            $competitorId = CompetitorTable::getOrCreateByDomain($comp['domain']);

            // Save to history before updating
            $current = CompetitorPriceTable::getByProductAndCompetitor($productId, $competitorId);
            if ($current && $current['PRICE'] != $comp['price']) {
                CompetitorPriceHistoryTable::add([
                    'PRODUCT_ID'    => $productId,
                    'COMPETITOR_ID' => $competitorId,
                    'PRICE'         => $current['PRICE'],
                    'RECORDED_AT'   => $current['CHECKED_AT'],
                ]);
            }

            CompetitorPriceTable::addOrUpdate([
                'PRODUCT_ID'    => $productId,
                'COMPETITOR_ID' => $competitorId,
                'PRICE'         => $comp['price'],
                'URL'           => $comp['url'],
                'IN_STOCK'      => $comp['in_stock'],
                'CHECKED_AT'    => new \Bitrix\Main\Type\DateTime(),
            ]);
        }
    }

    return __FUNCTION__ . '();';
}

Price Position Calculation

After each update we compute aggregates and position in bl_product_price_position:

-- Updated by a trigger or agent after synchronisation
INSERT INTO bl_product_price_position (product_id, our_price, min_comp, avg_comp, max_comp, rank, updated_at)
SELECT
    cp.product_id,
    bcp.PRICE as our_price,
    MIN(cp.price) as min_comp,
    ROUND(AVG(cp.price), 2) as avg_comp,
    MAX(cp.price) as max_comp,
    (SELECT COUNT(*) + 1 FROM bl_competitor_prices cp2
     WHERE cp2.product_id = cp.product_id AND cp2.price < bcp.PRICE) as rank,
    NOW()
FROM bl_competitor_prices cp
JOIN b_catalog_price bcp ON bcp.PRODUCT_ID = cp.product_id AND bcp.CATALOG_GROUP_ID = 1
GROUP BY cp.product_id, bcp.PRICE
ON CONFLICT (product_id) DO UPDATE SET
    our_price = EXCLUDED.our_price, min_comp = EXCLUDED.min_comp,
    avg_comp = EXCLUDED.avg_comp, rank = EXCLUDED.rank, updated_at = NOW();

Alerts on Competitor Price Changes

The agent compares new prices with previous ones and sends a notification to managers on significant changes (a competitor drops below our price):

foreach ($priceChanges as $change) {
    if ($change['new_price'] < $change['our_price'] && $change['old_price'] >= $change['our_price']) {
        // Competitor just became cheaper than us
        $message = sprintf(
            'Competitor %s has lowered the price on %s to %s (ours: %s)',
            $change['competitor_name'],
            $change['product_name'],
            number_format($change['new_price'], 2, '.', ' '),
            number_format($change['our_price'], 2, '.', ' ')
        );
        \Bitrix\Main\Mail\Event::send([
            'EVENT_NAME' => 'COMPETITOR_PRICE_ALERT',
            'LID'        => SITE_ID,
            'C_FIELDS'   => ['MESSAGE' => $message],
        ]);
    }
}

Timeline

Phase Duration
Database schema and repositories 2 days
Sync agent with data source 2 days
Position and aggregate calculations 1 day
Display in product card (admin panel) 2 days
Change notifications 1 day
Testing 1 day
Total 9–10 days