Tour operators face a problem: prices change daily, departure dates fluctuate, and external booking systems respond at different speeds. One returns JSON in 200 ms, another returns XML in 8 seconds. If the catalog is designed as a static information block with manual updates, within a month managers stop updating prices, and customers book tours with outdated data. We are a development team with 10 years of experience in 1C-Bitrix, specializing in certified Bitrix development and proven architecture that solves these problems. Our approach guarantees the site works with real data in real time. Over 10 years, we have implemented more than 50 projects for tour operators—from small showcases to complex platforms with B2B agent portals. Basic project costs start from $5,000 for a simple showcase, saving clients up to 40% on operational costs through automation. For a typical catalog with one API integration, clients save an average of $15,000 per year on manual updates.
How to Build a Tour Catalog on Information Blocks
The catalog is built on two information blocks and one Highload-block. This architecture provides flexibility and performance even with thousands of tours.
Information block "Destinations" — sections of the tour information block. Hierarchy: "Europe" → "Italy" → "Tuscany". Section properties via UF_* fields: UF_COUNTRY_CODE (ISO 3166-1), UF_CLIMATE_INFO, UF_VISA_REQUIRED, UF_GALLERY. Sections are used both for filtering and for SEO optimization with Schema.org markup for structured data.
Information block "Tours" (type tours) — elements inside destination sections. Properties include duration, departure dates, tour type, complexity, group size, included services, base price, hotel stars, external booking system identifier, gallery, and video. Linking to the trade catalog (CCatalog::Add()) is only needed if booking goes through the sale module. If payment goes to an external system (Samo.Tourvisor), Bitrix catalog is not connected—the information block works as a showcase.
Highload-block "Price Coefficients" (PriceCoefficients) stores dynamic pricing rules: fields TOUR_ID, DATE_FROM, DATE_TO, COEFFICIENT, RULE_TYPE (early booking, hot deal, seasonal, group discount), PRIORITY. Highload is chosen because there will be thousands of records—each tour × each season × each rule type. Query via \Bitrix\Highloadblock\HighloadBlockTable::getList() with filter by TOUR_ID and current date.
The Need for a Custom Facet Filter Component
The standard catalog.smart.filter is not suitable for tours: departure dates are a multiple property, price is calculated dynamically, destination is a section hierarchy. We develop a custom component project:tour.filter that processes requests 3 times faster than the standard one on a catalog of 2000 tours.
Filtering by destination. Hierarchical selection: country → region → resort. When a country is selected, the filter loads regions via an AJAX request to /api/tours/regions/?country=IT. In component.php — CIBlockSection::GetList() with caching via CPHPCache with tag iblock_id_N.
Filtering by departure dates. The client selects a range—for example, "from June 1 to June 30". In the database, dates are stored as multiple property values. Problem: standard CIBlockElement::GetList() with such a filter works slowly on large volumes. Solution—intermediate table b_tour_departure_index, populated by an agent when the tour is updated. Filtering uses a JOIN on this table, result is an array of TOUR_ID.
Filtering by price. Price is calculated at request time: BASE_PRICE × season coefficient × early booking coefficient. Direct filtering is impossible. Two options exist:
- Materialized price — an agent recalculates the current price of each tour once an hour and writes it to the CURRENT_PRICE property. Filtering by it is standard. Downside: up to an hour delay.
- Two-stage filtering — first select tours by all other filters, then for each calculate the price and cut off those not within the range. Works accurately, but with a catalog of 5000 tours, the second stage can take 200–500 ms. This is solved by caching calculated prices in Redis with a TTL of 15 minutes.
In practice, we choose the first option—the client sees a price with a margin of error up to an hour, but filtering works instantly. The exact price is shown on the tour detail page and during checkout.
AJAX filtering. All filters are sent as one GET request: /api/tours/search/?destination=IT&date_from=01.06&date_to=30.06&duration_min=7&duration_max=10&price_max=2000&type=excursion. The controller in local/modules/project.tours/lib/controller/search.php inherits \Bitrix\Main\Engine\Controller, validates parameters, builds the filter for CIBlockElement::GetList(), returns JSON with an array of tours and facet metadata (how many tours for each type under current filters). Facets—counters next to each filter value—are calculated with separate COUNT(*) queries, cached for 5 minutes.
How Do We Handle Dynamic Pricing?
Three pricing levels:
- Seasonal coefficients — high season ×1.3, low season ×0.8. Stored in Highload-block PriceCoefficients with date ranges.
- Early booking — discount 10–20% when booking 60+ days before departure. Rule: if DEPARTURE_DATE - TODAY > 60, apply coefficient 0.85.
- Hot deals — discount 15–40% for 3–7 days before departure when group is unfilled. Coefficient depends on fill percentage: GROUP_FILLED < 50% → 0.6.
Price calculation in \Project\Tours\PriceCalculator::calculate($tourId, $departureDate):
Click to see the price calculation algorithm
$basePrice = $tour['BASE_PRICE']; $coefficients = HighloadBlockTable::getList([ 'filter' => [ 'TOUR_ID' => $tourId, '<=DATE_FROM' => $departureDate, '>=DATE_TO' => $departureDate, ], 'order' => ['PRIORITY' => 'ASC'], ])->fetchAll();
$finalPrice = $basePrice; foreach ($coefficients as $c) { $finalPrice *= $c['COEFFICIENT']; }
Priority determines the application order. Seasonal coefficient (priority 1) is applied first, then early booking (priority 2), then hot deal (priority 3). Rules do not conflict: early booking and hot deal are mutually exclusive by design.
Integrating Samo.Tourvisor and Sletat
A tour operator rarely sells only their own tours. The site aggregates offers from multiple sources: own tours (in information block), package tours from Samo.Tourvisor, and offers from Sletat.ru.
Samo.Tourvisor API — RESTful JSON. Main endpoints:
- GET /api/search — search tours by parameters (country, resort, date, nights, adults/children). Response: an array of offers with price, hotel, departure date, operator.
- GET /api/hotel/{id} — hotel details: photos, description, coordinates.
- POST /api/order — create a booking request.
Integration is implemented via a module local/modules/project.tourvisor/. Class \Project\Tourvisor\Client wraps HTTP requests via \Bitrix\Main\Web\HttpClient. Critical point — response time. Samo.Tourvisor responds in 2–8 seconds. The user should not wait: first load shows results from the local information block (own tours) instantly; in parallel, the frontend sends an AJAX request to /api/tourvisor/search/, the backend caches the result in Redis with a TTL of 30 minutes.
Sletat.ru API — XML/SOAP. Old protocol, but huge tour database. Main method — GetTours(). Response time — 5–15 seconds. Feature — RequestId: first request returns RequestId, which must be polled via GetSearchResult() every 2–3 seconds until status becomes Completed. This polling mechanism is implemented on the frontend.
Merging results from different sources. On the frontend — a single list with source mark. Each result contains source (local / tourvisor / sletat), external_id, price, currency. Sorting by price requires currency conversion using the Central Bank rate, stored in a Highload-block CurrencyRates and updated by an agent once a day.
Steps to Set Up Facet Filtering
Here is a step-by-step guide to implementing facet filtering:
- Define destination hierarchy in the information block sections.
- Create the intermediate table
b_tour_departure_indexand populate it via an agent. - Build the custom component
project:tour.filterwith AJAX endpoints. - Implement materialized price updates using an agent.
- Cache facet counts with a short TTL (5 minutes).
- Test with a catalog of 2000 tours to ensure performance.
What Is Included in the Work (Deliverables)
- Architectural documentation (database schema, module structure, API integration descriptions)
- Remote access setup and source code transfer
- Training managers to work with the admin panel and filters
- Post-release support for 3 months (bug fixes, consultations)
Common mistakes during implementation:
- Lack of indexes on multiple properties leads to slow filtering.
- Ignoring external API timeouts — user sees infinite loading.
- Synchronous call to all booking sources — page loads 10+ seconds.
| Project Scale | Estimated Timeline |
|---|---|
| Showcase of own tours without online booking | 3–5 weeks |
| Catalog with filtering, booking, one integration (Tourvisor) | 6–10 weeks |
| Full platform: multiple APIs, B2B portal, dynamic pricing | 10–14 weeks |
| Source | API Type | Typical Response Time | Integration Complexity |
|---|---|---|---|
| Local information block | — | < 50 ms | Low |
| Samo.Tourvisor | REST JSON | 2–8 sec | Medium |
| Sletat.ru | XML/SOAP | 5–15 sec | High (polling) |
We specialize in tour operator website development, including SEO optimization and tour booking systems. Contact us to discuss the architecture of your catalog. Get a consultation on timelines and cost—we will calculate an individual plan. Our engineers with certified 1C-Bitrix expertise and 10 years of experience guarantee a performant solution that avoids common pitfalls.







