import asyncio import logging from datetime import datetime, timezone, timedelta import httpx from aiogram import Bot, Dispatcher from aiogram.client.default import DefaultBotProperties from aiogram.enums import ParseMode from aiogram.types import MenuButtonWebApp, WebAppInfo from config import load_config, Config from handlers import start log = logging.getLogger("zov.bot") MSK = timezone(timedelta(hours=3)) def _years_word(n: int) -> str: """Правильное склонение: год / года / лет.""" if n % 100 in (11, 12, 13, 14): return "лет" if n % 10 == 1: return "год" if n % 10 in (2, 3, 4): return "года" return "лет" async def _send_anniversary_reminders(bot: Bot, config: Config) -> None: """Вызывает /api/daily_reminders и рассылает уведомления менеджерам.""" if not config.internal_secret: log.warning("INTERNAL_SECRET не задан — рассылка годовщин пропущена") return url = f"{config.backend_url}/api/daily_reminders" headers = {"Authorization": f"Bearer {config.internal_secret}"} try: async with httpx.AsyncClient(timeout=30) as client: resp = await client.get(url, headers=headers) resp.raise_for_status() data = resp.json() except Exception as e: log.error("Ошибка запроса к /api/daily_reminders: %s", e) return reminders = data.get("reminders", []) log.info("Годовщины договоров: %d записей на %s", len(reminders), data.get("date", "?")) for r in reminders: manager_tg_id = r.get("manager_tg_id", "") client_name = r.get("client_name", "Клиент") years: int = r.get("years", 1) contract_date_raw = r.get("contract_date", "") # Форматируем дату для отображения date_str = contract_date_raw for fmt in ("%Y-%m-%d", "%d.%m.%Y"): try: date_str = datetime.strptime(contract_date_raw[:10], fmt).strftime("%d.%m.%Y") break except ValueError: continue word = _years_word(years) text = ( f"📋 Годовщина договора!\n\n" f"Сегодня ровно {years} {word} как вы подписали договор " f"с {client_name}.\n" f"📅 Дата договора: {date_str}\n\n" f"Отличный повод напомнить о себе и предложить новые услуги 💼" ) try: await bot.send_message(chat_id=int(manager_tg_id), text=text) log.info("Годовщина отправлена менеджеру %s (клиент: %s, лет: %d)", manager_tg_id, client_name, years) except Exception as e: log.warning("Не удалось отправить уведомление менеджеру %s: %s", manager_tg_id, e) async def _anniversary_scheduler(bot: Bot, config: Config) -> None: """Фоновая задача: каждый день в 09:00 МСК рассылает годовщины договоров.""" while True: now = datetime.now(MSK) next_run = now.replace(hour=9, minute=0, second=0, microsecond=0) if now >= next_run: next_run += timedelta(days=1) delay = (next_run - now).total_seconds() log.info( "Планировщик годовщин: следующий запуск через %.0f сек (%s МСК)", delay, next_run.strftime("%d.%m %H:%M"), ) await asyncio.sleep(delay) await _send_anniversary_reminders(bot, config) # Короткая пауза чтобы не сработало дважды при граничном времени await asyncio.sleep(60) async def main() -> None: logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", ) config = load_config() bot = Bot( token=config.bot_token, default=DefaultBotProperties(parse_mode=ParseMode.HTML), ) dp = Dispatcher() dp["config"] = config dp.include_router(start.router) if config.use_webhook: raise NotImplementedError("Webhook mode будет добавлен после MVP") # Универсальная меню-кнопка — открывает MiniApp одним тапом. # Внутри MiniApp пользователь выбирает роль (менеджер/клиент/сотрудник). try: await bot.set_chat_menu_button( menu_button=MenuButtonWebApp( text="CRM", web_app=WebAppInfo(url=config.miniapp_url), ), ) logging.info("Установлена меню-кнопка MiniApp: %s", config.miniapp_url) except Exception as e: logging.warning("Не удалось установить меню-кнопку: %s", e) logging.info("Запуск в режиме polling") await bot.delete_webhook(drop_pending_updates=True) # Запускаем фоновый планировщик годовщин asyncio.create_task(_anniversary_scheduler(bot, config)) logging.info("Планировщик годовщин запущен (09:00 МСК ежедневно)") await dp.start_polling(bot) if __name__ == "__main__": asyncio.run(main())