Разработка платформы для аренды жилья

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.
Разработка и обслуживание любых видов сайтов:
Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Разработка платформы для аренды жилья
Сложная
от 2 недель до 3 месяцев
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1214
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    852
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    815

Разработка платформы для аренды жилья

Платформа аренды жилья — это не просто сайт с объявлениями. Это транзакционная система, где ошибка в логике бронирования или баг в расчёте доступности стоит реальных денег и репутации. Airbnb потратил годы на отладку calendar sync и конфликтов двойного бронирования — и всё равно периодически с этим борется. Здесь разбираем, как строить подобные системы грамотно с первого раза.

Архитектура данных: самое сложное место

Ядро любой rental-платформы — модель доступности. Наивный подход через available: boolean на объекте не работает. Нужна отдельная таблица периодов:

CREATE TABLE availability_blocks (
    id          BIGSERIAL PRIMARY KEY,
    listing_id  BIGINT NOT NULL REFERENCES listings(id),
    start_date  DATE NOT NULL,
    end_date    DATE NOT NULL,
    block_type  VARCHAR(20) NOT NULL, -- 'booked', 'owner_blocked', 'maintenance'
    booking_id  BIGINT REFERENCES bookings(id),
    CHECK (end_date > start_date)
);

CREATE INDEX idx_availability_listing_dates
    ON availability_blocks (listing_id, start_date, end_date);

Для проверки доступности на запрашиваемый период:

SELECT COUNT(*) = 0 AS is_available
FROM availability_blocks
WHERE listing_id = $1
  AND block_type IN ('booked', 'owner_blocked')
  AND start_date < $3  -- requested end
  AND end_date > $2;   -- requested start

Запрос должен выполняться под блокировкой SELECT FOR UPDATE при создании бронирования — иначе race condition при одновременных запросах на один объект.

Поиск с геофильтрацией

PostGIS — стандарт для geo-поиска. Минимальная конфигурация:

CREATE EXTENSION IF NOT EXISTS postgis;

ALTER TABLE listings
    ADD COLUMN location GEOGRAPHY(POINT, 4326);

CREATE INDEX idx_listings_location
    ON listings USING GIST(location);

Поиск объектов в радиусе с фильтрами:

SELECT
    l.id,
    l.title,
    l.price_per_night,
    ST_Distance(l.location, ST_MakePoint($1, $2)::geography) AS distance_meters
FROM listings l
WHERE ST_DWithin(
    l.location,
    ST_MakePoint($1, $2)::geography,
    $3  -- radius in meters
)
  AND l.guests_max >= $4
  AND l.bedrooms >= $5
  AND NOT EXISTS (
      SELECT 1 FROM availability_blocks ab
      WHERE ab.listing_id = l.id
        AND ab.block_type IN ('booked', 'owner_blocked')
        AND ab.start_date < $7
        AND ab.end_date > $6
  )
ORDER BY distance_meters
LIMIT 50;

На фронте карту реализуем через Mapbox GL JS или Leaflet + OpenStreetMap. Mapbox дороже, но векторные тайлы и кастомные стили — значительно лучший UX для аренды жилья.

Система бронирования: состояния и переходы

Бронирование — это конечный автомат. Нарушение порядка переходов = баги с деньгами:

pending_payment → confirmed → active → completed
                ↘ cancelled
confirmed → cancelled_by_host / cancelled_by_guest
active → disputed

Реализация на Laravel с паттерном State:

class Booking extends Model
{
    public function confirm(): void
    {
        if ($this->status !== BookingStatus::PendingPayment) {
            throw new InvalidBookingTransitionException(
                "Cannot confirm booking in status: {$this->status->value}"
            );
        }

        DB::transaction(function () {
            $this->update(['status' => BookingStatus::Confirmed]);

            AvailabilityBlock::create([
                'listing_id' => $this->listing_id,
                'start_date' => $this->check_in,
                'end_date'   => $this->check_out,
                'block_type' => 'booked',
                'booking_id' => $this->id,
            ]);

            event(new BookingConfirmed($this));
        });
    }

    public function cancel(string $initiator, ?string $reason = null): void
    {
        $allowed = [BookingStatus::PendingPayment, BookingStatus::Confirmed];

        if (!in_array($this->status, $allowed)) {
            throw new InvalidBookingTransitionException();
        }

        DB::transaction(function () use ($initiator, $reason) {
            $status = $initiator === 'host'
                ? BookingStatus::CancelledByHost
                : BookingStatus::CancelledByGuest;

            $this->update(['status' => $status, 'cancel_reason' => $reason]);

            AvailabilityBlock::where('booking_id', $this->id)->delete();

            $this->processRefund();

            event(new BookingCancelled($this, $initiator));
        });
    }
}

Платежи и удержание средств

Ключевая особенность rental: деньги удерживаются при бронировании и передаются хозяину после заезда (или после checkout). Stripe Connect — стандарт для маркетплейсов:

// Создание платёжного намерения с захватом средств
$paymentIntent = $stripe->paymentIntents->create([
    'amount'               => $booking->total_amount_cents,
    'currency'             => 'usd',
    'capture_method'       => 'manual', // не списываем сразу
    'application_fee_amount' => $booking->platform_fee_cents,
    'transfer_data'        => [
        'destination' => $host->stripe_account_id,
    ],
    'metadata'             => [
        'booking_id' => $booking->id,
    ],
]);

// После подтверждения заезда — захват
$stripe->paymentIntents->capture($paymentIntent->id);

Для расчёта дат выплаты хозяину — обычно T+24h после check-in или через 24h после checkout (зависит от политики платформы). Реализуется через scheduled jobs:

// app/Jobs/ProcessHostPayout.php
class ProcessHostPayout implements ShouldQueue
{
    public function handle(): void
    {
        Booking::query()
            ->where('status', BookingStatus::Active)
            ->where('check_in', '<=', now()->subDay())
            ->whereNull('payout_processed_at')
            ->each(function (Booking $booking) {
                $this->stripe->transfers->create([
                    'amount'      => $booking->host_payout_cents,
                    'currency'    => 'usd',
                    'destination' => $booking->listing->host->stripe_account_id,
                ]);

                $booking->update(['payout_processed_at' => now()]);
            });
    }
}

Синхронизация с внешними каналами (iCal)

Хозяева часто размещают объекты на нескольких платформах. Нужна синхронизация через iCal (RFC 5545):

use ICal\ICal;

class ICalSyncService
{
    public function import(Listing $listing, string $icalUrl): void
    {
        $ical = new ICal($icalUrl, ['defaultTimeZone' => 'UTC']);

        DB::transaction(function () use ($listing, $ical) {
            // Удаляем старые внешние блокировки
            AvailabilityBlock::where('listing_id', $listing->id)
                ->where('block_type', 'external_ical')
                ->delete();

            foreach ($ical->events() as $event) {
                AvailabilityBlock::create([
                    'listing_id' => $listing->id,
                    'start_date' => Carbon::parse($event->dtstart)->toDateString(),
                    'end_date'   => Carbon::parse($event->dtend)->toDateString(),
                    'block_type' => 'external_ical',
                    'external_uid' => $event->uid,
                ]);
            }
        });
    }

    public function export(Listing $listing): string
    {
        $calendar = new \Eluceo\iCal\Domain\Entity\Calendar();

        AvailabilityBlock::where('listing_id', $listing->id)
            ->where('block_type', 'booked')
            ->get()
            ->each(function ($block) use ($calendar) {
                $event = (new \Eluceo\iCal\Domain\Entity\Event())
                    ->setOccurrence(
                        new \Eluceo\iCal\Domain\ValueObject\DateTimeSpan(
                            new \Eluceo\iCal\Domain\ValueObject\DateTime(
                                Carbon::parse($block->start_date), false
                            ),
                            new \Eluceo\iCal\Domain\ValueObject\DateTime(
                                Carbon::parse($block->end_date), false
                            )
                        )
                    )
                    ->setSummary('Booked');

                $calendar->addEvent($event);
            });

        return (new \Eluceo\iCal\Presentation\Factory\CalendarFactory())
            ->createCalendar($calendar);
    }
}

Синхронизация запускается через Laravel Scheduler каждые 15-30 минут для активных листингов.

Система отзывов с двусторонней анонимностью

На Airbnb обе стороны оставляют отзыв до определённой даты, и отзывы публикуются одновременно — чтобы исключить давление. Реализация:

Schema::create('reviews', function (Blueprint $table) {
    $table->id();
    $table->foreignId('booking_id')->unique()->constrained();
    $table->foreignId('reviewer_id')->constrained('users');
    $table->foreignId('reviewee_id')->constrained('users');
    $table->enum('reviewer_type', ['guest', 'host']);
    $table->tinyInteger('rating'); // 1-5
    $table->text('comment');
    $table->timestamp('submitted_at');
    $table->timestamp('published_at')->nullable(); // null пока не опубликован
});

Публикация через Job, который проверяет: оба ли отзыва сданы, или прошёл ли дедлайн (14 дней после checkout):

class PublishReviewsJob implements ShouldQueue
{
    public function handle(): void
    {
        $bookingIds = Review::query()
            ->select('booking_id')
            ->whereNull('published_at')
            ->groupBy('booking_id')
            ->havingRaw('COUNT(*) = 2 OR MIN(submitted_at) < ?', [
                now()->subDays(14)
            ])
            ->pluck('booking_id');

        Review::whereIn('booking_id', $bookingIds)
            ->whereNull('published_at')
            ->update(['published_at' => now()]);
    }
}

Уведомления: реальное время и email

WebSocket для уведомлений о новых бронированиях реализуем через Laravel Echo + Pusher или Soketi (self-hosted):

class BookingCreated implements ShouldBroadcast
{
    public function broadcastOn(): array
    {
        return [
            new PrivateChannel("host.{$this->booking->listing->host_id}"),
        ];
    }

    public function broadcastWith(): array
    {
        return [
            'booking_id'  => $this->booking->id,
            'guest_name'  => $this->booking->guest->name,
            'check_in'    => $this->booking->check_in->format('d.m.Y'),
            'check_out'   => $this->booking->check_out->format('d.m.Y'),
            'total'       => $this->booking->total_amount,
        ];
    }
}

Сроки разработки

Базовая версия (поиск, листинги, бронирование, Stripe, кабинеты хозяина/гостя): 10–12 недель.

Добавление iCal-синхронизации, двустороннего ревью, карты с кластеризацией, фильтров, мобильной адаптации: ещё 4–6 недель.

Полноценный запуск с модерацией объявлений, верификацией личности, dispute resolution, аналитикой для хозяев: 20–24 недели от старта.

Самый длинный этап — не разработка, а тестирование граничных случаев с бронированием и выплатами. Здесь не бывает «достаточно хорошо» — каждый баг либо потеря денег, либо юридический риск.