A hotel website differs from a regular catalog in that visitors choose a time slot, not a product. A room itself is a set of characteristics (area, capacity, view). But without available dates, it's a dead card. The entire project revolves around the availability calendar and booking mechanics, not around beautiful layout. The problem: visitors see nice photos, but when trying to book — the calendar is not updated or the price does not match the season. Or there is no integration with Booking.com, and the hotel gets overbooked. We solve these problems at the architecture level: booking engine on Highload-blocks, seasonal rates, PMS synchronization.
How the booking system works?
The core of the project is managing room availability. Task: user selects dates, system shows available types with prices, books and locks the room.
Storage of availability — Highload-block RoomInventory. Each row is one night for one physical room.
| Field | Type | Description |
|---|---|---|
| UF_DATE | date | Date (e.g., 2025-07-15 — night from July 15 to July 16) |
| UF_ROOM_ID | integer | ID of the physical room |
| UF_ROOM_TYPE_ID | integer | ID of the room type |
| UF_STATUS | integer | 0 = free, 1 = booked, 2 = blocked, 3 = occupied |
| UF_BOOKING_ID | integer | Order ID (from sale module) |
| UF_RATE | float | Rate for this night (with season applied) |
Highload-block is an ORM wrapper over a table with automatic API. With 100 rooms and a 365-day horizon — 36,500 rows. Required indexes: composite on (UF_DATE, UF_ROOM_TYPE_ID, UF_STATUS) and (UF_BOOKING_ID).
Algorithm for checking availability. Guest enters check_in, check_out, guests. System finds room types with at least one physical room free for all nights.
public function getAvailableRoomTypes(
\Bitrix\Main\Type\Date $checkIn,
\Bitrix\Main\Type\Date $checkOut,
int $guests
): array {
$nights = $checkOut->getDiff($checkIn)->days;
$dates = [];
for ($i = 0; $i < $nights; $i++) {
$d = clone $checkIn;
$d->add(new \DateInterval("P{$i}D"));
$dates[] = $d->format('Y-m-d');
}
// Find rooms occupied at least one night
$busyRooms = RoomInventoryTable::getList([
'select' => ['UF_ROOM_ID'],
'filter' => [
'UF_DATE' => $dates,
'!UF_STATUS' => 0,
],
'group' => ['UF_ROOM_ID'],
])->fetchAll();
$busyRoomIds = array_column($busyRooms, 'UF_ROOM_ID');
// Next — exclude occupied rooms and filter by capacity
}
The approach using HAVING COUNT(*) = {$nights} is more correct:
SQL query for availability check
SELECT UF_ROOM_ID, UF_ROOM_TYPE_ID
FROM hl_room_inventory
WHERE UF_DATE IN ('2025-07-15','2025-07-16','2025-07-17')
AND UF_STATUS = 0
GROUP BY UF_ROOM_ID, UF_ROOM_TYPE_ID
HAVING COUNT(*) = 3
Frontend availability calendar. Two fields — check-in and check-out. Implementation with flatpickr in range mode. On open — AJAX request for an availability matrix: array of dates with a flag "free rooms available". Endpoint returns JSON:
{
"2025-07": {
"15": {"available": true, "min_rate": 4500},
"16": {"available": true, "min_rate": 4500},
"17": {"available": false, "min_rate": null},
"18": {"available": true, "min_rate": 6200}
}
}
Unavailable dates are blocked in the calendar (disable). Minimum rate — on hover. Queries are cached via Bitrix\Main\Data\Cache with key availability_{month}_{year}.
Why seasonal pricing increases profit?
Highload-block RatePlan:
| Field | Type |
|---|---|
| UF_ROOM_TYPE_ID | integer |
| UF_DATE_FROM | date |
| UF_DATE_TO | date |
| UF_WEEKDAY_RATE | float |
| UF_WEEKEND_RATE | float |
| UF_PRIORITY | integer |
When calculating booking cost, the system iterates over each night, finds the applicable rate plan, and sums. Total amount is the sum across all nights.
Room inventory architecture
Each room type is an infoblock element "Room inventory". It is important to separate the type (e.g., "Standard Double") and physical rooms (20 rooms). This is a key architectural decision.
Infoblock structure:
| Property | Type | Purpose |
|---|---|---|
| CAPACITY | N (number) | Capacity (main beds) |
| CAPACITY_EXTRA | N | Extra beds (foldaway, baby cot) |
| AREA | N | Area, m² |
| AMENITIES | L (list, multiple) | Amenities: Wi-Fi, air conditioning, minibar, safe |
| BED_TYPE | L (list) | Bed type: double, twin, king |
| VIEW | L (list) | View: sea, city, garden, courtyard |
| FLOOR_RANGE | S (string) | Floors: "3-5" |
| GALLERY | F (file, multiple) | Photo gallery of the room |
| PANORAMA_URL | S | Link to 360-panorama |
| ROOM_COUNT | N | Number of physical rooms of this type |
| MIN_STAY | N | Minimum number of nights |
| BASE_RATE | N | Base rate per night (without seasonal surcharges) |
Amenities (AMENITIES) — multiple property of type "List". Not a Highload-block, because the set is fixed (30-50 items). On the frontend, values are mapped to icons via config.
Photo gallery and virtual tour. Multiple property of type "File". Rendering — Swiper.js with lazy-loading, preview via CFile::ResizeImageGet() at 600x400 with BX_RESIZE_IMAGE_PROPORTIONAL. For 360-panorama — Pannellum.js: library takes equirectangular image and renders interactive view. Storage — string property PANORAMA_URL. Pannellum is initialized on the client:
pannellum.viewer('panorama-container', {
type: 'equirectangular',
panorama: roomData.panoramaUrl,
autoLoad: true,
compass: true,
hotSpots: [
{ pitch: -5, yaw: 120, type: 'info', text: 'Bathroom' },
{ pitch: 0, yaw: 240, type: 'info', text: 'Balcony with sea view' }
]
});
Hotspots are set in a JSON infoblock property or in a Highload-block if an admin panel is needed.
How iCal synchronization prevents overbooking?
The hotel sells rooms on the website and through OTAs (Booking.com, Expedia). Without synchronization — overbooking. A Channel Manager syncs availability and rates between PMS, website, and channels.
iCal synchronization — the simplest option. Booking.com and Airbnb provide .ics files. A Bitrix agent runs every 15 minutes:
- Fetches
.icsvia URL (file_get_contentsor cURL) - Parses
VEVENT— extractsDTSTART,DTEND,SUMMARY - Updates
RoomInventory:UF_STATUS = 2(blocked) - Generates outgoing
.icswith bookings from the website
iCal limitation: no rates, up to 15-minute delay. For 100+ rooms, an API connector is needed. XML Push / API — via REST or SOAP (Booking.com Connectivity API). iCal synchronization is 10x simpler than API integration, but less flexible.
PMS integration. 1C:Hotel — exchange via HTTP service. Opera / Fidelio — SOAP with WSDL. We implement a wrapper class over SoapClient with logging.
Online payment and prepayment. Booking via sale module. Order — one item "Stay in {type}, {check_in} — {check_out}". Prepayment (20-30%) — custom handler OnSaleBeforeOrderAdd. Percentage and first night are typical schemes.
Additional modules
Guest personal account: authorization (email + OAuth), my bookings, travel history, loyalty program. Points are accrued via handler OnSaleStatusOrderChange.
Multilingual: language versions (/en/, /de/), content via infoblock properties, hreflang.
SEO and microdata: Schema.org Hotel + HotelRoom + Offer. Markup is generated automatically.
Photo-centric design: WebP with fallback, <picture> with srcset, lazy-load. Originals up to 3000px, preview 800x600.
Reviews: Highload-block Reviews with moderation. Aggregated rating in infoblock property AVG_RATING.
What is included in the work
- Analytics and prototyping (room inventory map, booking logic)
- Design (photo-centric UI, mobile version, calendar)
- Booking core (RoomInventory, availability check, order processing, payment)
- Integrations (PMS, Channel Manager, payment systems, REST API)
- Content and SEO (microdata, multilingual, meta templates)
- Testing and launch (load testing, cross-browser, deployment)
- Documentation and admin training
- Post-project support and warranty
Timeline and stages
| Scale | Timeline |
|---|---|
| Mini-hotel, 10-20 rooms, basic booking | 4–8 weeks |
| Hotel, 50-100 rooms, Channel Manager, PMS | 10–16 weeks |
| Hotel chain, multisite, loyalty program | 16–24 weeks |
Timelines do not include photography and 360-panorama creation — that runs in parallel.
Direct booking through the website is 2-3 times more profitable for the hotel than through OTAs, due to no commission. The savings are 15-25% of revenue. The booking system pays for itself on average in 6-12 months due to increased direct sales.
We have been working in hotel development for over 8 years and have completed more than 50 projects for the hotel industry. Our engineers are certified 1C-Bitrix specialists. Order the development of a hotel website — we will select an architecture for your room inventory. Get a consultation on the architecture of your site — we will assess the task and propose a solution.







