Limgro
КейсУ продакшені з 2026RentalE-commerceMulti-tenant

SeRental

Прокат техніки на мультитенантній CMS

SeRental — прокат знімальної техніки: камери, оптика, світло, стабілізація, від однієї доби. Вітрина — сайт на Next.js без власної бази: кожен товар, ціна, категорія і спосіб оплати приходять із Nexus, мультитенантної CMS нашої розробки, через публічний API з авторизацією по ключу. Оренда — одна з семи вертикалей, які обслуговує та сама платформа.

Next.js
React
TypeScript
Laravel
PostgreSQL
Redis
GraphQL
Цифри
138
Одиниць у каталозі
13
Категорій
7
Вертикалей на платформі

Виклик

Прокат — це не магазин. Той самий об'єктив вільний наступного тижня і зайнятий завтра, ціна залежить від того, наскільки довго ви його тримаєте, а два клієнти можуть забронювати ту саму одиницю в ту саму секунду. Плюс власник хотів знижки за тривалість без переписування драбини на 138 позицій і передоплату карткою, яка сама повертається, якщо бронювання не підтвердили.

Що ми зробили

Наявність вирішується в кошику, проти реальних бронювань, а календар гасить дати, на які комплект не стане. Знижки — одна спільна сітка відсотків, адресована на все, на категорію або на окрему позицію, і перемагає найвужчий адресат. Подвійне бронювання відмовляє сама база — exclusion constraint Postgres по діапазонах часу, а не код, який може програти гонку. Оплата карткою несе адресу колбеку в кожному рахунку, а скасування оплаченого бронювання повертає гроші тією ж дією.

Результат

Жива вітрина на платформі, якою власник керує сам: ціни, тарифи, категорії, послуги і способи оплати редагуються в адмінці й з'являються на сайті без деплою. Оплата, повернення і наявність тримаються на сервері, тож те, що бачить клієнт, і те, що з нього беруть, не може розійтися.

Що вміє продукт

Кожен екран нижче — живий продукт, знятий цього тижня.

Головна — пошук і категорії по всьому парку.
Модуль 01

Вітрина

Прокат обирають за тим, що в ньому є і як швидко знаходиться потрібне, тому вхід — це пошук і категорія, а не буклет.

  • 138 одиниць у 13 категоріях, пораховані з живого каталогу
  • Пошук за назвою, категорії — прямо з адмінки
  • Кожна цифра на сторінці виведена, жодної зашитої
Об'єктиви — фасети з лічильниками, що відповідають «а якщо перемкнути».
Модуль 02

Каталог і фасети

Фільтри будуються з характеристик, які завів оператор — байонет, формат матриці, тип оптики — і кожен лічильник рахується з усіма іншими фільтрами, але без себе самого.

  • Лічильник не звужує власну групу, тож перемикання ніколи не показане нулем
  • Ціна, наявність і характеристики — один спільний шар запитів
  • Один запит сторінки плюс один прохід на всі незаймані осі, хоч який розмір каталогу
Картка товару — сітка, переведена в ціни цієї позиції.
Модуль 03

Тарифи

Ставка за добу падає з тривалістю оренди. Драбина написана один раз у відсотках і цінує весь магазин — від тримача за 100 ₴ до камери за 4 500 ₴.

  • Одна сітка, адресована на все, на категорію або на окрему позицію
  • Перемагає найвужчий адресат; одна активна сітка на адресата — тримає індекс
  • Смуги не можуть перетинатися — відмовляє range exclusion constraint
Кошик — дати, наявність, знижка й оплата в одному місці.
Модуль 04

Бронювання й оплата

Один період на весь комплект. Наявність перевіряється проти реальних бронювань, знижка названа в гривнях до того, як людина щось підтвердила, а картковий шлях закінчується бронюванням, яке оператор повертає однією дією.

  • Календар гасить дати, на які позиція комплекту вже зайнята
  • Повна сума, знижка й підсумок названі окремо, а не згорнуті в одне число
  • Картка або готівка; непідтверджене бронювання повертає гроші саме
На телефоні

Кошик — місце, де сходяться дати, наявність і ціна. Зроблений спершу під телефон, зі знижкою, названою в гривнях.

Під капотом

Стек і інженерні рішення, на яких це зроблено.

Фронтенд
Next.js 16 App Router · React 19 · TypeScript · Tailwind v4
Бекенд
Laravel 12 · Lighthouse GraphQL · UseCase layer · public REST API
Дані
PostgreSQL 17 database-per-tenant · btree_gist exclusion constraints · Redis
Рендеринг та i18n
ISR storefront · server actions · availability re-checked under lock
Інтеграції
Card acquiring with per-invoice webhooks · refunds · Telegram ops channel
Інженерні рішення
  • Одна драбина знижок на весь каталог — відсотки, а не таблиця, переписана в кожну позицію.
  • Перетин бронювань відмовляє exclusion constraint у Postgres, тож два одночасні оформлення не можуть виграти обидва.
  • Кошик питає в API сьогоднішні ціни, а не вірить тому, що зберіг при додаванні позиції.
  • Скасування оплаченого бронювання робить повернення тією ж дією; шлюз, який відмовив, каже про це, а не звітує про успіх.

Маєте схожий проект?

Давайте поговоримо про те, як ми можемо створити щось чудове для вашого бізнесу. Ми любимо виклики.

Почати розмову

Давайте створимо щось чудове разом

Маєте проект на думці? Розкажіть нам про це. Ми відповімо протягом 24 годин з чітким планом дій.