Розробка платформи для аренди жилья
Платформа аренди жилля — це не просто сайт з об'явами. Це трансакційна система, де помилка в логіці бронювання або баг у розрахунку доступності коштує реальних грошей та репутації. 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));
});
}
}
Платежі та утримання коштів
Ключова особливість rental: гроші утримуються при бронюванні та передаються господарю після заїзду (або після checkout). Stripe Connect — стандарт для маркетплейсів.
Синхронізація з зовнішніми каналами (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',
]);
}
});
}
}
Синхронізація запускається кожні 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 поки не опубліковано
});
Повідомлення: реальний час та 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}"),
];
}
}
Терміни розробки
Базова версія (пошук, листинги, бронювання, Stripe, кабінети господаря/гостя): 10–12 тижнів.
Додавання iCal-синхронізації, двустороннього ревю, карти з кластеризацією, фільтрів, мобільної адаптації: ще 4–6 тижнів.
Повний запуск з модерацією об'явлень, верифікацією особистості, dispute resolution, аналітикою для господарів: 20–24 тижні від старту.
Найдовший етап — не розробка, а тестування граничних випадків з бронюванням та виплатами. Тут не бувает «достатньо добре» — кожен баг або втрата грошей, або юридичний ризик.







