Legan Studio
Все статьи
~ 4 мин чтения

Бот для автосервиса в MAX: запись, история ТО, напоминания

Как сделать бот для автосервиса в MAX: онлайн-запись, история обслуживания по VIN, напоминания о ТО, расчёт стоимости работ, интеграция с 1С и СТО-системами.

  • MAX
  • автосервис
  • вертикали

Автосервис в мессенджере — практичная история без шоу: клиент пишет «нужна замена масла на Logan 2018», бот за 30 секунд отдаёт стоимость и свободное окно завтра в 14:00. После работы — напоминание через 8 000 км или 6 месяцев. История ТО по VIN — всегда под рукой. В этой статье — полная анатомия бота для автосервиса в MAX: запись по марке/модели/работе, расчёт стоимости из прайса, история обслуживания по VIN, напоминания, отзывы, интеграция с 1С УНФ / СТО-софтом (АвтоДилер, СТО-онлайн).

Что должен уметь бот автосервиса

  1. Запись на услугу с подбором свободного слота.
  2. Автоматический расчёт стоимости работ (норма-час + запчасти).
  3. Хранение истории обслуживания по VIN / госномеру.
  4. Напоминания: следующее ТО, замена ремня ГРМ, OSAGO/КАСКО.
  5. Эвакуатор / выездной мастер.
  6. Загрузка фото проблемы для предварительной диагностики.
  7. Отзывы и NPS.
  8. Программа лояльности (накопительные скидки).

Модель данных

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 0005%
150 000–300 00010%
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

  1. VIN без валидации — клиенты вводят 16 символов вместо 17, поиск ломается.
  2. Слот без блокировки — два клиента бронируют одновременно один пост.
  3. Прайс «забит» в код — при изменении норма-часа правят разработчики.
  4. Напоминания каждый день — клиент отписывается. Не чаще 1 раза в 60 дней на каждый тип.
  5. Нет привязки автомобиля к нескольким пользователям — муж записал, жена не видит историю.

Итого

Бот для автосервиса в 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% через автоматические предложения сопутствующих работ, удержание клиентов через регулярные напоминания о ТО.