§ 01О чём проектWhat this is
Типичный бьюти-салон в Ташкенте ведёт запись через WhatsApp и телефонные звонки. Администратор жонглирует бумажным журналом, тремя чатами и памятью. Результат предсказуем: двойные записи на одного мастера, потерянные клиенты, которые не дождались ответа, и no-show без напоминаний. A typical beauty salon in Tashkent handles bookings via WhatsApp and phone calls. The receptionist juggles a paper ledger, three chats, and memory. The result is predictable: double-bookings for the same master, lost clients who never got a reply, and no-shows without reminders.
Проблемы ручной записи: клиент пишет в WhatsApp — ответ через 2 часа, когда слот уже занят. Два мастера записаны на одно время. Клиент не приходит, потому что забыл — час простоя стоит салону 150–200 тыс. сум. Manual booking problems: a client texts WhatsApp — gets a reply 2 hours later when the slot is already taken. Two masters booked at the same time. A client doesn't show up because they forgot — an idle hour costs the salon 150–200k UZS.
Задача: бот, который позволяет клиенту записаться за 30 секунд без звонков и ожидания, а администратору — управлять расписанием без Excel и бумаги. Telegram — потому что в Узбекистане это мессенджер номер один. Goal: a bot that lets clients book in 30 seconds without calls or waiting, and lets admins manage the schedule without Excel or paper. Telegram — because it's the number one messenger in Uzbekistan.
§ 02Как устроеноHow it's built
FSM-первый дизайн. Запись — это стейт-машина из шести шагов: услуга → дополнения → мастер → дата → время → подтверждение. Каждый шаг валидирует входные данные и проверяет доступность слота в реальном времени. Aiogram 3 FSM идеально ложится на этот поток. FSM-first design. A booking is a state machine of six steps: service → add-ons → master → date → time → confirmation. Each step validates input and checks slot availability in real time. Aiogram 3 FSM maps perfectly to this flow.
┌──────────────────────────────────────────────────────────┐ │ /book │ │ │ │ │ ▼ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ Service │─▶│ Add-ons │─▶│ Master │ │ │ │ select │ │ select │ │ select │ │ │ └──────────┘ └──────────┘ └────┬─────┘ │ │ │ │ │ ┌───────────────────────────┘ │ │ ▼ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ Date │─▶│ Time │─▶│ Confirm │ │ │ │ pick │ │ slot │ │ + pay │ │ │ └──────────┘ └──────────┘ └────┬─────┘ │ │ │ │ │ ┌───────────────────────────┘ │ │ ▼ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ SQLite WAL · Redis cache · APScheduler reminders │ │ │ └──────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────┘
Доменный слой полностью независим от транспорта. Бизнес-логика (проверка слотов, расчёт стоимости, управление расписанием) живёт отдельно от Telegram-хендлеров. SQLite WAL выбран осознанно: одна база на салон, бэкап — cp файла, нет сетевых зависимостей. Redis кеширует расписание мастеров и доступные слоты.
The domain layer is fully transport-independent. Business logic (slot checks, price calculation, schedule management) lives separately from Telegram handlers. SQLite WAL was a deliberate choice: one database per salon, backup is a file cp, zero network dependencies. Redis caches master schedules and available slots.
§ 03Модель лицензированияLicensing model
Каждый салон получает годовую лицензию. Ключ подписан Ed25519 — offline-верификация без обращения к серверу лицензий. Grace-период 90 дней: если лицензия истекла, бот продолжает работать, но показывает предупреждение администратору. Это честный подход — салон не теряет данные и записи клиентов. Each salon gets an annual license. The key is signed with Ed25519 — offline verification with no license server calls. 90-day grace period: if the license expires, the bot keeps running but shows a warning to the admin. A fair approach — the salon doesn't lose data or client bookings.
def verify_license(key_b64: str, pub: Ed25519PublicKey) -> License: # Офлайн-проверка, без обращения к серверу payload = base64.b64decode(key_b64) signature, data = payload[:64], payload[64:] pub.verify(signature, data) license = json.loads(data) expires = date.fromisoformat(license["expires"]) grace = expires + timedelta(days=90) if date.today() > grace: raise LicenseExpired(salon=license["salon"]) if date.today() > expires: logger.warning("License expired, grace period active") return License(**license)
Исходный код открыт для аудита заказчиком, но деплой требует валидный ключ. Модель «source-available, deployment-locked» — салон видит, что внутри, но не может поднять второй инстанс без лицензии. Source code is open for client audit, but deployment requires a valid key. The "source-available, deployment-locked" model — the salon sees what's inside but can't spin up a second instance without a license.
§ 04ЦифрыNumbers
Автобэкап каждые 6 часов: APScheduler копирует SQLite-файл и отправляет в приватный Telegram-канал. Восстановление — скачать файл и положить на место. Никаких внешних сервисов, никаких S3-бакетов. Auto-backup every 6 hours: APScheduler copies the SQLite file and sends it to a private Telegram channel. Recovery — download the file and drop it in place. No external services, no S3 buckets.
§ 05Что я вынесLessons learned
(а) UX салона — не UX SaaS. Мастер между клиентами смотрит в телефон 10 секунд. Любой экран с больше чем тремя кнопками — провал. Inline-клавиатуры максимально плоские: один ряд выбора, одна кнопка подтверждения. Каждый лишний тап — потерянная запись. (a) Salon UX is not SaaS UX. A master checks their phone for 10 seconds between clients. Any screen with more than three buttons is a failure. Inline keyboards are kept maximally flat: one row of choices, one confirm button. Every extra tap is a lost booking.
(б) Self-hosted бьёт SaaS для единичных салонов. Владелец не хочет ежемесячную подписку на платформу и не хочет, чтобы его клиентская база жила на чужом сервере. VPS за $5/мес + годовая лицензия — модель, которую понимают и принимают. (b) Self-hosted beats SaaS for individual salons. The owner doesn't want a monthly platform subscription and doesn't want their client base living on someone else's server. A $5/mo VPS + annual license — a model they understand and accept.
(в) Лицензирование — не ограничение, а фича продукта. Ed25519-ключ решает три задачи: монетизация, защита от клонирования, и доверие (салон может проверить, что код не изменён). Grace-период снимает страх «бот отключится и я потеряю записи». (c) Licensing is not a restriction — it's a product feature. The Ed25519 key solves three problems: monetization, clone protection, and trust (the salon can verify the code hasn't been tampered with). The grace period removes the fear of "the bot will shut down and I'll lose my bookings."