Автосервис в мессенджере — практичная история без шоу: клиент пишет «нужна замена масла на Logan 2018», бот за 30 секунд отдаёт стоимость и свободное окно завтра в 14:00. После работы — напоминание через 8 000 км или 6 месяцев. История ТО по VIN — всегда под рукой. В этой статье — полная анатомия бота для автосервиса в MAX: запись по марке/модели/работе, расчёт стоимости из прайса, история обслуживания по VIN, напоминания, отзывы, интеграция с 1С УНФ / СТО-софтом (АвтоДилер, СТО-онлайн).
Что должен уметь бот автосервиса
- Запись на услугу с подбором свободного слота.
- Автоматический расчёт стоимости работ (норма-час + запчасти).
- Хранение истории обслуживания по VIN / госномеру.
- Напоминания: следующее ТО, замена ремня ГРМ, OSAGO/КАСКО.
- Эвакуатор / выездной мастер.
- Загрузка фото проблемы для предварительной диагностики.
- Отзывы и NPS.
- Программа лояльности (накопительные скидки).
Модель данных
CREATE TABLE customers (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT UNIQUE,
phone TEXT,
name TEXT
);
CREATE TABLE cars (
id BIGSERIAL PRIMARY KEY,
customer_id BIGINT REFERENCES customers(id),
vin TEXT,
plate TEXT,
brand TEXT,
model TEXT,
year INT,
mileage INT,
last_mileage_at TIMESTAMPTZ
);
CREATE INDEX ix_cars_vin ON cars(vin);
CREATE INDEX ix_cars_plate ON cars(plate);
CREATE TABLE services (
id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL,
norm_hours NUMERIC(4,2),
base_price NUMERIC(10,2),
category TEXT -- engine, brakes, electric, oils, ...
);
CREATE TABLE bookings (
id BIGSERIAL PRIMARY KEY,
car_id BIGINT REFERENCES cars(id),
customer_id BIGINT REFERENCES customers(id),
services JSONB, -- [{"id":1,"price":2500},...]
parts JSONB, -- [{"sku":"X","qty":1,"price":1200},...]
total NUMERIC(10,2),
slot_at TIMESTAMPTZ,
duration_min INT,
status TEXT -- new|confirmed|in_progress|done|cancelled
);
CREATE TABLE service_history (
id BIGSERIAL PRIMARY KEY,
car_id BIGINT REFERENCES cars(id),
booking_id BIGINT REFERENCES bookings(id),
services JSONB,
parts JSONB,
mileage INT,
notes TEXT,
created_at TIMESTAMPTZ
);
Сценарий записи
[Старт] → "Какая машина?" → [выбор из списка / добавить новую по VIN]
→ "Что нужно сделать?" → [категории → услуги]
→ расчёт стоимости + длительности
→ выбор свободного слота
→ подтверждение → бронь
@bot.callback_query_handler(lambda c: c.data == "menu:book")
async def on_book(call):
cars = await db.fetch_all("SELECT * FROM cars WHERE customer_id = $1", await get_customer_id(call.from_user.id))
rows = [[InlineKeyboardButton(f"{c.brand} {c.model} {c.plate}", callback_data=f"car:{c.id}")] for c in cars]
rows.append([InlineKeyboardButton("➕ Добавить авто", callback_data="car:new")])
await bot.send_message(call.message.chat.id, "Выберите автомобиль:", reply_markup=InlineKeyboardMarkup(rows))
Расчёт стоимости
async def estimate(services_ids: list[int], car: Car) -> dict:
services = await db.fetch_all("SELECT * FROM services WHERE id = ANY($1)", services_ids)
NORM_HOUR_PRICE = 2_500 # стоимость нормо-часа на этом СТО
work_total = sum(s.norm_hours * NORM_HOUR_PRICE for s in services)
parts = await pick_parts_for(services, car) # подбор запчастей по модели
parts_total = sum(p["price"] * p["qty"] for p in parts)
duration_min = int(sum(s.norm_hours for s in services) * 60)
return {
"work_total": work_total,
"parts": parts,
"parts_total": parts_total,
"total": work_total + parts_total,
"duration_min": duration_min,
}
Подбор запчастей по марке/модели/году — отдельная история: справочник OEM-каталогов (Laximo, СтартАвто), привязка к складскому остатку 1С/АвтоДилер.
Поиск свободного слота
Слоты — окна по 30 минут, постов несколько (1–5). Алгоритм: ищем первое окно, где влезает duration_min подряд хотя бы на одном посту, без конфликтов с уже забронированными.
async def find_slots(date_from: datetime, duration_min: int, n: int = 5) -> list[datetime]:
bookings = await db.fetch_all("""
SELECT slot_at, duration_min, post_id
FROM bookings
WHERE slot_at >= $1 AND slot_at < $2 AND status IN ('confirmed','in_progress')
""", date_from, date_from + timedelta(days=7))
busy = build_busy_map(bookings) # post_id -> list of (start, end)
free = []
cur = date_from
while len(free) < n and cur < date_from + timedelta(days=14):
if can_fit(busy, cur, duration_min):
free.append(cur)
cur += timedelta(minutes=30)
return free
История ТО по VIN
После каждого визита — запись в service_history с пробегом, работами и запчастями. Команда /history:
@bot.message_handler(commands=["history"])
async def on_history(msg):
car = await pick_car(msg.from_user.id)
if not car:
return await bot.send_message(msg.chat.id, "Сначала добавьте автомобиль")
history = await db.fetch_all("""
SELECT * FROM service_history
WHERE car_id = $1
ORDER BY created_at DESC LIMIT 20
""", car.id)
text = f"История *{car.brand} {car.model} {car.plate}*\n\n"
for h in history:
text += f"• {h.created_at:%d.%m.%Y} — {h.mileage:,} км\n"
for s in h.services:
text += f" – {s['title']} ({s['price']}₽)\n"
await bot.send_message(msg.chat.id, text, parse_mode="Markdown")
Напоминания
-- материализованное представление для крон-задач
CREATE MATERIALIZED VIEW upcoming_to AS
SELECT
c.id AS car_id,
c.customer_id,
last.mileage + 10000 AS next_to_mileage,
last.created_at + interval '6 months' AS next_to_date
FROM cars c
JOIN LATERAL (
SELECT mileage, created_at FROM service_history
WHERE car_id = c.id AND services @> '[{"category":"oil_change"}]'::jsonb
ORDER BY created_at DESC LIMIT 1
) last ON true;
async def remind_to_cron():
rows = await db.fetch_all("""
SELECT car_id, customer_id, next_to_mileage, next_to_date
FROM upcoming_to
WHERE next_to_date <= now() + interval '14 days'
AND NOT EXISTS (
SELECT 1 FROM reminders
WHERE car_id = upcoming_to.car_id AND kind='to'
AND sent_at > now() - interval '60 days'
)
""")
for r in rows:
await bot.send_message(
r.customer_user_id,
f"🛢 Через 2 недели нужно ТО (масло, фильтры). Записать?",
reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Записать", callback_data=f"book:to:{r.car_id}")]]),
)
await mark_reminder_sent(r.car_id, "to")
Аналогично — напоминания о замене ремня ГРМ, страховке, диагностике перед сезоном.
Эвакуатор и выездной мастер
@bot.callback_query_handler(lambda c: c.data == "service:tow")
async def on_tow(call):
kb = ReplyKeyboardMarkup([[KeyboardButton("📍 Отправить локацию", request_location=True)]],
one_time_keyboard=True)
await bot.send_message(call.message.chat.id, "Где находитесь?", reply_markup=kb)
@bot.message_handler(content_types=["location"], func=lambda m: state(m) == "tow:location")
async def on_tow_location(msg):
await create_tow_request(msg.from_user.id, msg.location.latitude, msg.location.longitude)
await bot.send_message(msg.chat.id, "Эвакуатор выехал. Ориентировочное время прибытия 25 минут.")
Фото для предварительной диагностики
Клиент шлёт фото царапины / повреждения. Бот пересылает мастеру, мастер даёт оценку, бот отвечает клиенту:
@bot.message_handler(content_types=["photo"])
async def on_photo(msg):
file_id = msg.photo[-1].file_id
request_id = await create_diag_request(msg.from_user.id, file_id, msg.caption)
await bot.send_photo(MASTER_CHAT_ID, photo=file_id,
caption=f"#{request_id} от {msg.from_user.id}: {msg.caption or ''}")
await bot.send_message(msg.chat.id, "Фото принято, мастер ответит в течение часа.")
Программа лояльности
Накопительная скидка по сумме чеков за 12 месяцев:
| Сумма за год | Скидка |
|---|---|
| < 50 000 ₽ | 0% |
| 50 000–150 000 | 5% |
| 150 000–300 000 | 10% |
| 300 000+ | 15% |
В боте — команда /loyalty показывает текущий уровень, накопленную сумму, до следующего уровня.
Интеграция с 1С УНФ и СТО-софтом
- Заказ-наряд из бота создаётся в 1С автоматически (REST API 1С).
- Изменение статуса в 1С (work_in_progress, done) — синхронизируется обратно в бота.
- Прайс синхронизируется раз в час.
- Складские остатки — раз в 15 минут (для отображения «есть/нет в наличии»).
async def create_order_in_1c(booking: Booking):
payload = {
"Дата": booking.slot_at.isoformat(),
"Контрагент": booking.customer.name,
"Телефон": booking.customer.phone,
"Автомобиль": {"VIN": booking.car.vin, "ГосНомер": booking.car.plate},
"Услуги": [{"Услуга": s["id"], "Стоимость": s["price"]} for s in booking.services],
"ЗапЧасти": booking.parts,
}
async with httpx.AsyncClient(auth=(USER_1C, PASS_1C)) as client:
r = await client.post(f"{ONEC_BASE}/hs/auto/order", json=payload)
r.raise_for_status()
Common pitfalls
- VIN без валидации — клиенты вводят 16 символов вместо 17, поиск ломается.
- Слот без блокировки — два клиента бронируют одновременно один пост.
- Прайс «забит» в код — при изменении норма-часа правят разработчики.
- Напоминания каждый день — клиент отписывается. Не чаще 1 раза в 60 дней на каждый тип.
- Нет привязки автомобиля к нескольким пользователям — муж записал, жена не видит историю.
Итого
Бот для автосервиса в MAX автоматизирует запись с подбором свободного слота, расчётом стоимости (нормо-час + запчасти) и интеграцией с 1С УНФ или АвтоДилер, хранит историю ТО по VIN с пробегом и работами, напоминает о следующем ТО / замене ремня / страховке через cron на материализованных представлениях, обрабатывает заявки на эвакуатор по геолокации, принимает фото для предварительной диагностики, ведёт накопительную программу лояльности. MVP — 4–6 недель и 700–1500 тыс. ₽; полная версия с эвакуатором, выездом мастера, lояльностью, интеграцией 1С/Laximo/АвтоДилер — 8–14 недель и 2–4 млн ₽. Окупаемость — 3–6 месяцев за счёт снижения no-show, роста среднего чека через предложенные допработы и удержания клиентов через напоминания.
Частые вопросы
Как привязать автомобиль к клиенту в боте?
Минимально — через ручной ввод VIN (17 символов, валидация контрольной цифры) и/или госномера. Дополнительно — фото СТС/ПТС с распознаванием через Yandex Vision. Один клиент может иметь несколько машин (личная + жены + рабочая), один автомобиль — нескольких авторизованных пользователей (член семьи, водитель компании). При первом обращении — заполнение марки, модели, года, текущего пробега. История пробега обновляется при каждом визите автоматически.
Как рассчитать стоимость работ в боте?
По формуле «нормо-часы × стоимость нормо-часа + запчасти». Норма-час хранится в таблице services (нормативные времена по работам), стоимость нормо-часа — настройка СТО. Запчасти подбираются по VIN/модели через справочники Laximo/СтартАвто или из складского остатка 1С/АвтоДилер. Итог = работа + запчасти. Длительность визита = сумма норма-часов × 60 минут. Финальный счёт может скорректировать мастер при выявлении дополнительных проблем — бот покажет «оригинальная сумма + доп. работы», требует подтверждения клиента.
Как реализовать поиск свободного слота?
Слоты — окна по 15–30 минут, у автосервиса несколько постов (1–10). Алгоритм: пройти по календарю с шагом 15 минут, для каждого слота проверить, влезает ли требуемая длительность подряд хотя бы на одном свободном посту. Учитывать рабочие часы, обед, выходные. Слот резервируется атомарно с FOR UPDATE — два клиента не смогут забронировать одно окно. Перед окончательным подтверждением — повторная проверка доступности (gracefully handle конфликт).
Как настроить напоминания о ТО?
По двум критериям: пробег (типично каждые 10 000 км для масла, 60 000 км для ГРМ) и время (6 месяцев для масла, 2 года для антифриза). Используйте материализованное представление с расчётом next_to_date/next_to_mileage по последнему визиту соответствующей категории. Cron раз в день шлёт напоминание за 14 дней до даты, не чаще одного на каждый тип ремонта в 60 дней (анти-спам). Кнопка «Записать» — сразу прыгает в FSM записи на эту работу.
Как интегрировать бот с 1С УНФ или АвтоДилер?
1С УНФ — через REST API (расширение «HTTP-сервисы»): бот создаёт заказ-наряд через POST, обновления статуса работ — через GET по таймеру или push с 1С. АвтоДилер и СТО-онлайн обычно имеют свои REST/SOAP API. Прайс синхронизируется раз в час, складские остатки — каждые 15 минут. Для двунаправленной синхронизации нужен webhook от 1С/СТО-софта при изменении статуса наряда — иначе клиент не узнает, что машина готова. Подробнее в статье «Интеграция бота MAX с 1С/amoCRM/Bitrix24».
Как обработать выездной ремонт и эвакуатор?
Кнопка «🚛 Эвакуатор» / «🔧 Выездной мастер» → запрос геолокации (request_location), описание проблемы, фото при необходимости. Заявка попадает в группу диспетчеров, рассчитывается стоимость по тарифу + расстоянию, диспетчер назначает ближайшего эвакуатора/мастера, клиенту приходит время прибытия и ссылка на трекинг (если интеграция с GPS-сервисом). Оплата — после выполнения работы через ЮKassa-ссылку в боте. Это закрывает 80% запросов без звонков и ускоряет обработку с 15–20 минут до 2–3.
Сколько стоит и сколько занимает разработка?
MVP с записью, прайсом, историей по VIN, базовыми напоминаниями — 4–6 недель и 700–1500 тыс. ₽. Полная версия с подбором запчастей по Laximo, эвакуатором, выездным мастером, программой лояльности, интеграцией 1С УНФ — 8–14 недель и 2–4 млн ₽. Поддержка — 10–25 тыс. ₽/мес. Для среднего автосервиса с 50–150 заявками в день окупаемость 3–6 месяцев: снижение no-show на 30–40% за счёт напоминаний, рост среднего чека на 15–25% через автоматические предложения сопутствующих работ, удержание клиентов через регулярные напоминания о ТО.