This commit is contained in:
@@ -116,6 +116,13 @@ class ApiClient:
|
||||
async def register_service_center(self, telegram_id: int, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
return await self.request("POST", "/api/service-centers", telegram_id=telegram_id, json=payload)
|
||||
|
||||
async def accept_sto_invite(self, telegram_id: int, invite_token: str) -> dict[str, Any]:
|
||||
return await self.request(
|
||||
"POST",
|
||||
f"/api/service-centers/employees/invites/{invite_token}/accept",
|
||||
telegram_id=telegram_id,
|
||||
)
|
||||
|
||||
async def pending_service_centers(self, telegram_id: int) -> list[dict[str, Any]]:
|
||||
return await self.request("GET", "/api/admin/service-centers/pending", telegram_id=telegram_id)
|
||||
|
||||
|
||||
141
bot/main.py
141
bot/main.py
@@ -24,6 +24,33 @@ logging.basicConfig(level=logging.INFO)
|
||||
dp = Dispatcher()
|
||||
api = ApiClient()
|
||||
|
||||
APPROVED_SERVICE_STATUSES = {"approved", "verified"}
|
||||
STO_WORKPLACE_ROLES = {"owner", "mechanic"}
|
||||
|
||||
|
||||
def webapp_url(path: str = "") -> str:
|
||||
base = settings.effective_webapp_url.rstrip("/")
|
||||
if not path:
|
||||
return base
|
||||
return f"{base}/{path.lstrip('/')}"
|
||||
|
||||
|
||||
def sto_workplace_url() -> str:
|
||||
return webapp_url("sto.html")
|
||||
|
||||
|
||||
async def sto_workplace_centers(telegram_id: int) -> list[dict]:
|
||||
try:
|
||||
centers = await api.my_service_centers(telegram_id)
|
||||
except httpx.HTTPStatusError:
|
||||
return []
|
||||
return [
|
||||
center
|
||||
for center in centers
|
||||
if center.get("verification_status") in APPROVED_SERVICE_STATUSES
|
||||
and (center.get("employee_role") or "owner") in STO_WORKPLACE_ROLES
|
||||
]
|
||||
|
||||
|
||||
def main_keyboard() -> ReplyKeyboardMarkup:
|
||||
return ReplyKeyboardMarkup(
|
||||
@@ -35,28 +62,39 @@ def main_keyboard() -> ReplyKeyboardMarkup:
|
||||
)
|
||||
|
||||
|
||||
def webapp_inline_keyboard(text: str = "Открыть CarPass") -> InlineKeyboardMarkup:
|
||||
async def main_keyboard_for(telegram_id: int) -> ReplyKeyboardMarkup:
|
||||
rows = [
|
||||
[KeyboardButton(text="Меню"), KeyboardButton(text="Мои авто")],
|
||||
]
|
||||
if await sto_workplace_centers(telegram_id):
|
||||
rows.append([KeyboardButton(text="Панель СТО")])
|
||||
rows.append([KeyboardButton(text="Помощь")])
|
||||
return ReplyKeyboardMarkup(keyboard=rows, resize_keyboard=True)
|
||||
|
||||
|
||||
def webapp_inline_keyboard(text: str = "Открыть CarPass", path: str = "") -> InlineKeyboardMarkup:
|
||||
return InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[InlineKeyboardButton(text=text, web_app=WebAppInfo(url=settings.effective_webapp_url))],
|
||||
[InlineKeyboardButton(text=text, web_app=WebAppInfo(url=webapp_url(path)))],
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def menu_inline_keyboard() -> InlineKeyboardMarkup:
|
||||
return InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[InlineKeyboardButton(text="Открыть Mini App", web_app=WebAppInfo(url=settings.effective_webapp_url))],
|
||||
[
|
||||
InlineKeyboardButton(text="Мои авто", callback_data="menu:garage"),
|
||||
InlineKeyboardButton(text="Аналитика", callback_data="menu:analytics"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="Добавить запись", callback_data="menu:add_record"),
|
||||
InlineKeyboardButton(text="СТО", callback_data="menu:sto"),
|
||||
],
|
||||
]
|
||||
)
|
||||
def menu_inline_keyboard(include_sto_workplace: bool = False) -> InlineKeyboardMarkup:
|
||||
rows = [
|
||||
[InlineKeyboardButton(text="Открыть Mini App", web_app=WebAppInfo(url=webapp_url()))],
|
||||
[
|
||||
InlineKeyboardButton(text="Мои авто", callback_data="menu:garage"),
|
||||
InlineKeyboardButton(text="Аналитика", callback_data="menu:analytics"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="Добавить запись", callback_data="menu:add_record"),
|
||||
InlineKeyboardButton(text="СТО", callback_data="menu:sto"),
|
||||
],
|
||||
]
|
||||
if include_sto_workplace:
|
||||
rows.append([InlineKeyboardButton(text="Панель СТО", web_app=WebAppInfo(url=sto_workplace_url()))])
|
||||
return InlineKeyboardMarkup(inline_keyboard=rows)
|
||||
|
||||
|
||||
def admin_card_keyboard(center_id: int) -> InlineKeyboardMarkup:
|
||||
@@ -127,17 +165,19 @@ async def start(message: Message) -> None:
|
||||
"CarPass ведет цифровой паспорт автомобиля: заправки, ТО, страховку, штрафы, стоимость владения и подтвержденную историю СТО.\n\n"
|
||||
"Mini App открывай кнопкой под сообщением: так Telegram передает защищенную авторизацию."
|
||||
)
|
||||
await message.answer(text, reply_markup=menu_inline_keyboard())
|
||||
await message.answer("Клавиатура ниже открывает команды бота.", reply_markup=main_keyboard())
|
||||
centers = await sto_workplace_centers(message.from_user.id)
|
||||
await message.answer(text, reply_markup=menu_inline_keyboard(include_sto_workplace=bool(centers)))
|
||||
await message.answer("Клавиатура ниже открывает команды бота.", reply_markup=await main_keyboard_for(message.from_user.id))
|
||||
|
||||
|
||||
@dp.message(F.text == "Меню")
|
||||
@dp.message(Command("menu"))
|
||||
async def menu(message: Message) -> None:
|
||||
await upsert(message)
|
||||
centers = await sto_workplace_centers(message.from_user.id)
|
||||
await message.answer(
|
||||
"Главное меню CarPass. Быстрые команды доступны здесь, а полный интерфейс работает в Mini App.",
|
||||
reply_markup=menu_inline_keyboard(),
|
||||
reply_markup=menu_inline_keyboard(include_sto_workplace=bool(centers)),
|
||||
)
|
||||
|
||||
|
||||
@@ -315,13 +355,9 @@ async def appointments(message: Message) -> None:
|
||||
@dp.message(Command("sto_bookings"))
|
||||
async def sto_bookings(message: Message) -> None:
|
||||
await upsert(message)
|
||||
try:
|
||||
centers = await api.my_service_centers(message.from_user.id)
|
||||
except httpx.HTTPStatusError as error:
|
||||
await message.answer(f"Кабинет СТО не доступен: {error.response.text}")
|
||||
return
|
||||
centers = await sto_workplace_centers(message.from_user.id)
|
||||
if not centers:
|
||||
await message.answer("У вас пока нет СТО. Подайте заявку через /register_sto или Mini App.")
|
||||
await message.answer("Панель СТО доступна только владельцу подтвержденного СТО и активным механикам.")
|
||||
return
|
||||
center = centers[0]
|
||||
try:
|
||||
@@ -338,7 +374,40 @@ async def sto_bookings(message: Message) -> None:
|
||||
]
|
||||
for item in pending[:8]:
|
||||
lines.append(f"#{item['id']} {item['service_name']} — {item['requested_start_at'][:16].replace('T', ' ')}")
|
||||
await message.answer("\n".join(lines), reply_markup=webapp_inline_keyboard("Кабинет СТО"))
|
||||
await message.answer("\n".join(lines), reply_markup=webapp_inline_keyboard("Открыть панель СТО", "sto.html"))
|
||||
|
||||
|
||||
@dp.message(Command("sto_workplace"))
|
||||
@dp.message(F.text == "Панель СТО")
|
||||
async def sto_workplace(message: Message) -> None:
|
||||
await upsert(message)
|
||||
centers = await sto_workplace_centers(message.from_user.id)
|
||||
if not centers:
|
||||
await message.answer("Панель СТО доступна только владельцу подтвержденного СТО и активным механикам.")
|
||||
return
|
||||
center = centers[0]
|
||||
await message.answer(
|
||||
f"Панель СТО: {center.get('display_name') or center.get('name')}",
|
||||
reply_markup=webapp_inline_keyboard("Открыть панель СТО", "sto.html"),
|
||||
)
|
||||
|
||||
|
||||
@dp.message(Command("accept_sto_invite"))
|
||||
async def accept_sto_invite(message: Message, command: CommandObject) -> None:
|
||||
await upsert(message)
|
||||
token = command.args.strip() if command.args else ""
|
||||
if not token:
|
||||
await message.answer("Пришлите команду вместе с токеном: /accept_sto_invite <token>")
|
||||
return
|
||||
try:
|
||||
employee = await api.accept_sto_invite(message.from_user.id, token)
|
||||
except httpx.HTTPStatusError as error:
|
||||
await message.answer(f"Приглашение не принято: {error.response.text}")
|
||||
return
|
||||
await message.answer(
|
||||
f"Приглашение принято. Роль: {employee['role']}.",
|
||||
reply_markup=webapp_inline_keyboard("Открыть панель СТО", "sto.html"),
|
||||
)
|
||||
|
||||
|
||||
@dp.message(Command("register_sto"))
|
||||
@@ -508,6 +577,18 @@ async def admin_callback(callback: CallbackQuery) -> None:
|
||||
@dp.message(F.text == "Помощь")
|
||||
@dp.message(Command("help"))
|
||||
async def help_message(message: Message) -> None:
|
||||
centers = await sto_workplace_centers(message.from_user.id)
|
||||
sto_workplace_help = (
|
||||
"• /sto_bookings или /sto_workplace — панель подтвержденного СТО;\n"
|
||||
"• /accept_sto_invite <token> — принять приглашение сотрудника;\n"
|
||||
if centers
|
||||
else ""
|
||||
)
|
||||
sto_business_help = (
|
||||
"СТО: прими заявку, создай заказ-наряд, добавь работы/товары/жидкости, отправь владельцу на согласование и закрой работу после выполнения.\n"
|
||||
if centers
|
||||
else ""
|
||||
)
|
||||
await message.answer(
|
||||
"CarPass помогает вести цифровой паспорт автомобиля.\n\n"
|
||||
"Главное:\n"
|
||||
@@ -519,13 +600,13 @@ async def help_message(message: Message) -> None:
|
||||
"• /analytics — стоимость владения и расход;\n"
|
||||
"• /sto — каталог проверенных СТО;\n"
|
||||
"• /appointments — мои записи в СТО;\n"
|
||||
"• /sto_bookings — заявки и календарь для владельца СТО;\n"
|
||||
"• /register_sto — заявка на СТО.\n\n"
|
||||
f"{sto_workplace_help}"
|
||||
"\n"
|
||||
"Владелец: добавь авто, выбери проверенное СТО, создай запись, согласуй заказ-наряд и смотри завершенные работы в истории автомобиля.\n"
|
||||
"СТО: прими заявку, создай заказ-наряд, добавь работы/товары/жидкости, отправь владельцу на согласование и закрой работу после выполнения.\n"
|
||||
f"{sto_business_help}"
|
||||
"Безопасность: СТО видит автомобиль только после подтверждения владельца, а спорные изменения VIN, номера и пробега идут через согласование.\n\n"
|
||||
"Mini App открывай только кнопкой под сообщением: Telegram передает initData, и авторизация проходит корректно.",
|
||||
reply_markup=menu_inline_keyboard(),
|
||||
reply_markup=menu_inline_keyboard(include_sto_workplace=bool(centers)),
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user