mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-03-02 00:03:05 +00:00
Merge pull request #274 from Fr1ngg/cozprs-bedolaga/add-daily-summary-report-feature
Add scheduled admin reports and manual sending
This commit is contained in:
@@ -14,6 +14,11 @@ ADMIN_NOTIFICATIONS_ENABLED=true
|
||||
ADMIN_NOTIFICATIONS_CHAT_ID=-1001234567890 # Замени на ID твоего канала (-100) - ПРЕФИКС ЗАКРЫТОГО КАНАЛА! ВСТАВИТЬ СВОЙ ID СРАЗУ ПОСЛЕ (-100) БЕЗ ПРОБЕЛОВ!
|
||||
ADMIN_NOTIFICATIONS_TOPIC_ID=123 # Опционально: ID топика
|
||||
ADMIN_NOTIFICATIONS_TICKET_TOPIC_ID=126 # Опционально: ID топика для тикетов
|
||||
# Автоматические отчеты
|
||||
ADMIN_REPORTS_ENABLED=false
|
||||
ADMIN_REPORTS_CHAT_ID= # Опционально: чат для отчетов (по умолчанию ADMIN_NOTIFICATIONS_CHAT_ID)
|
||||
ADMIN_REPORTS_TOPIC_ID= # ID топика для отчетов
|
||||
ADMIN_REPORTS_SEND_TIME=10:00 # Время отправки (по МСК) ежедневного отчета
|
||||
# Обязательная подписка на канал
|
||||
CHANNEL_SUB_ID= # Опционально ID твоего канала (-100)
|
||||
CHANNEL_IS_REQUIRED_SUB=false # Обязательна ли подписка на канал
|
||||
|
||||
@@ -38,6 +38,7 @@ from app.handlers.admin import (
|
||||
backup as admin_backup,
|
||||
welcome_text as admin_welcome_text,
|
||||
tickets as admin_tickets,
|
||||
reports as admin_reports,
|
||||
)
|
||||
from app.handlers.stars_payments import register_stars_handlers
|
||||
|
||||
@@ -139,6 +140,7 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
|
||||
admin_backup.register_handlers(dp)
|
||||
admin_welcome_text.register_welcome_text_handlers(dp)
|
||||
admin_tickets.register_handlers(dp)
|
||||
admin_reports.register_handlers(dp)
|
||||
common.register_handlers(dp)
|
||||
register_stars_handlers(dp)
|
||||
logger.info("⭐ Зарегистрированы обработчики Telegram Stars платежей")
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import html
|
||||
from collections import defaultdict
|
||||
from datetime import time
|
||||
from typing import List, Optional, Union, Dict
|
||||
from pydantic_settings import BaseSettings
|
||||
from pydantic import field_validator, Field
|
||||
@@ -27,6 +29,11 @@ class Settings(BaseSettings):
|
||||
ADMIN_NOTIFICATIONS_TOPIC_ID: Optional[int] = None
|
||||
ADMIN_NOTIFICATIONS_TICKET_TOPIC_ID: Optional[int] = None
|
||||
|
||||
ADMIN_REPORTS_ENABLED: bool = False
|
||||
ADMIN_REPORTS_CHAT_ID: Optional[str] = None
|
||||
ADMIN_REPORTS_TOPIC_ID: Optional[int] = None
|
||||
ADMIN_REPORTS_SEND_TIME: Optional[str] = None
|
||||
|
||||
CHANNEL_SUB_ID: Optional[str] = None
|
||||
CHANNEL_LINK: Optional[str] = None
|
||||
CHANNEL_IS_REQUIRED_SUB: bool = False
|
||||
@@ -425,6 +432,32 @@ class Settings(BaseSettings):
|
||||
def format_price(self, price_kopeks: int) -> str:
|
||||
rubles = price_kopeks // 100
|
||||
return f"{rubles} ₽"
|
||||
|
||||
def get_reports_chat_id(self) -> Optional[str]:
|
||||
if self.ADMIN_REPORTS_CHAT_ID:
|
||||
return self.ADMIN_REPORTS_CHAT_ID
|
||||
return self.ADMIN_NOTIFICATIONS_CHAT_ID
|
||||
|
||||
def get_reports_topic_id(self) -> Optional[int]:
|
||||
return self.ADMIN_REPORTS_TOPIC_ID or None
|
||||
|
||||
def get_reports_send_time(self) -> Optional[time]:
|
||||
value = self.ADMIN_REPORTS_SEND_TIME
|
||||
if not value:
|
||||
return None
|
||||
|
||||
try:
|
||||
hours_str, minutes_str = value.strip().split(":", 1)
|
||||
hours = int(hours_str)
|
||||
minutes = int(minutes_str)
|
||||
if not (0 <= hours <= 23 and 0 <= minutes <= 59):
|
||||
raise ValueError
|
||||
return time(hour=hours, minute=minutes)
|
||||
except (ValueError, AttributeError):
|
||||
logging.getLogger(__name__).warning(
|
||||
"Некорректное значение ADMIN_REPORTS_SEND_TIME: %s", value
|
||||
)
|
||||
return None
|
||||
|
||||
def kopeks_to_rubles(self, kopeks: int) -> float:
|
||||
return kopeks / 100
|
||||
|
||||
85
app/handlers/admin/reports.py
Normal file
85
app/handlers/admin/reports.py
Normal file
@@ -0,0 +1,85 @@
|
||||
import logging
|
||||
from aiogram import Dispatcher, F, types
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database.models import User
|
||||
from app.keyboards.admin import get_admin_reports_keyboard
|
||||
from app.services.reporting_service import (
|
||||
ReportPeriod,
|
||||
ReportingServiceError,
|
||||
reporting_service,
|
||||
)
|
||||
from app.utils.decorators import admin_required, error_handler
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def show_reports_menu(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
) -> None:
|
||||
await callback.message.edit_text(
|
||||
"📊 <b>Отчеты</b>\n\n"
|
||||
"Выберите период, чтобы отправить отчет в админский топик.",
|
||||
reply_markup=get_admin_reports_keyboard(db_user.language),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def send_daily_report(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
) -> None:
|
||||
await _send_report(callback, ReportPeriod.DAILY)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def send_weekly_report(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
) -> None:
|
||||
await _send_report(callback, ReportPeriod.WEEKLY)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def send_monthly_report(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
) -> None:
|
||||
await _send_report(callback, ReportPeriod.MONTHLY)
|
||||
|
||||
|
||||
async def _send_report(callback: types.CallbackQuery, period: ReportPeriod) -> None:
|
||||
try:
|
||||
report_text = await reporting_service.send_report(period, send_to_topic=True)
|
||||
except ReportingServiceError as exc:
|
||||
logger.warning("Не удалось отправить отчет: %s", exc)
|
||||
await callback.answer(str(exc), show_alert=True)
|
||||
return
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.error("Непредвиденная ошибка при отправке отчета: %s", exc)
|
||||
await callback.answer("Не удалось отправить отчет. Попробуйте позже.", show_alert=True)
|
||||
return
|
||||
|
||||
await callback.message.answer(report_text)
|
||||
await callback.answer("Отчет отправлен в топик")
|
||||
|
||||
|
||||
def register_handlers(dp: Dispatcher) -> None:
|
||||
dp.callback_query.register(show_reports_menu, F.data == "admin_reports")
|
||||
dp.callback_query.register(send_daily_report, F.data == "admin_reports_daily")
|
||||
dp.callback_query.register(send_weekly_report, F.data == "admin_reports_weekly")
|
||||
dp.callback_query.register(send_monthly_report, F.data == "admin_reports_monthly")
|
||||
|
||||
@@ -11,6 +11,7 @@ def get_admin_main_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
[InlineKeyboardButton(text="👥 Юзеры/Подписки", callback_data="admin_submenu_users")],
|
||||
[InlineKeyboardButton(text="💰 Промокоды/Статистика", callback_data="admin_submenu_promo")],
|
||||
[InlineKeyboardButton(text="🛟 Поддержка", callback_data="admin_submenu_support")],
|
||||
[InlineKeyboardButton(text="📊 Отчеты", callback_data="admin_reports")],
|
||||
[InlineKeyboardButton(text="📨 Сообщения", callback_data="admin_submenu_communications")],
|
||||
[InlineKeyboardButton(text="⚙️ Настройки", callback_data="admin_submenu_settings")],
|
||||
[InlineKeyboardButton(text="🛠️ Система", callback_data="admin_submenu_system")],
|
||||
@@ -111,7 +112,7 @@ def get_admin_settings_submenu_keyboard(language: str = "ru") -> InlineKeyboardM
|
||||
|
||||
def get_admin_system_submenu_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
texts = get_texts(language)
|
||||
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="📄 Обновления", callback_data="admin_updates"),
|
||||
@@ -123,6 +124,15 @@ def get_admin_system_submenu_keyboard(language: str = "ru") -> InlineKeyboardMar
|
||||
])
|
||||
|
||||
|
||||
def get_admin_reports_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
return InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="📆 За вчера", callback_data="admin_reports_daily")],
|
||||
[InlineKeyboardButton(text="🗓️ За неделю", callback_data="admin_reports_weekly")],
|
||||
[InlineKeyboardButton(text="📅 За месяц", callback_data="admin_reports_monthly")],
|
||||
[InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_panel")]
|
||||
])
|
||||
|
||||
|
||||
def get_admin_users_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
return InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
|
||||
351
app/services/reporting_service.py
Normal file
351
app/services/reporting_service.py
Normal file
@@ -0,0 +1,351 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from datetime import date, datetime, time as datetime_time, timedelta, timezone
|
||||
from enum import Enum
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from aiogram import Bot
|
||||
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from app.config import settings
|
||||
from app.database.crud.subscription import get_subscriptions_statistics
|
||||
from app.database.database import AsyncSessionLocal
|
||||
from app.database.models import (
|
||||
Subscription,
|
||||
SubscriptionConversion,
|
||||
Transaction,
|
||||
TransactionType,
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ReportingServiceError(RuntimeError):
|
||||
"""Base error for the reporting service."""
|
||||
|
||||
|
||||
class ReportPeriod(Enum):
|
||||
DAILY = "daily"
|
||||
WEEKLY = "weekly"
|
||||
MONTHLY = "monthly"
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ReportPeriodRange:
|
||||
start_msk: datetime
|
||||
end_msk: datetime
|
||||
label: str
|
||||
|
||||
|
||||
class ReportingService:
|
||||
"""Generates admin summary reports and can schedule daily delivery."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.bot: Optional[Bot] = None
|
||||
self._task: Optional[asyncio.Task] = None
|
||||
self._moscow_tz = ZoneInfo("Europe/Moscow")
|
||||
|
||||
def set_bot(self, bot: Bot) -> None:
|
||||
self.bot = bot
|
||||
|
||||
def is_running(self) -> bool:
|
||||
return self._task is not None and not self._task.done()
|
||||
|
||||
async def start(self) -> None:
|
||||
await self.stop()
|
||||
|
||||
if not settings.ADMIN_REPORTS_ENABLED:
|
||||
logger.info("Сервис отчетов отключен настройками")
|
||||
return
|
||||
|
||||
if not self.bot:
|
||||
logger.warning("Невозможно запустить сервис отчетов без экземпляра бота")
|
||||
return
|
||||
|
||||
chat_id = settings.get_reports_chat_id()
|
||||
if not chat_id:
|
||||
logger.warning("Сервис отчетов не запущен: не указан чат для отправки отчетов")
|
||||
return
|
||||
|
||||
send_time = settings.get_reports_send_time()
|
||||
if not send_time:
|
||||
logger.warning("Сервис отчетов не запущен: не указано время ежедневной отправки")
|
||||
return
|
||||
|
||||
self._task = asyncio.create_task(self._auto_daily_loop(send_time))
|
||||
logger.info(
|
||||
"📊 Сервис отчетов запущен: ежедневная отправка в %s по МСК",
|
||||
send_time.strftime("%H:%M"),
|
||||
)
|
||||
|
||||
async def stop(self) -> None:
|
||||
if self._task and not self._task.done():
|
||||
self._task.cancel()
|
||||
try:
|
||||
await self._task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
self._task = None
|
||||
|
||||
async def send_report(
|
||||
self,
|
||||
period: ReportPeriod,
|
||||
*,
|
||||
report_date: Optional[date] = None,
|
||||
send_to_topic: bool = False,
|
||||
) -> str:
|
||||
report_text = await self._build_report(period, report_date)
|
||||
|
||||
if send_to_topic:
|
||||
await self._deliver_report(report_text)
|
||||
|
||||
return report_text
|
||||
|
||||
async def _auto_daily_loop(self, send_time: datetime_time) -> None:
|
||||
try:
|
||||
next_run_utc, report_date = self._calculate_next_run(send_time)
|
||||
|
||||
while True:
|
||||
now_utc = datetime.now(timezone.utc)
|
||||
delay = (next_run_utc - now_utc).total_seconds()
|
||||
|
||||
if delay > 0:
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
try:
|
||||
await self.send_report(
|
||||
ReportPeriod.DAILY,
|
||||
report_date=report_date,
|
||||
send_to_topic=True,
|
||||
)
|
||||
logger.info(
|
||||
"📊 Автоматический отчет за %s отправлен",
|
||||
report_date.strftime("%d.%m.%Y"),
|
||||
)
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.error("Ошибка автоматической отправки отчета: %s", exc)
|
||||
|
||||
next_run_utc, report_date = self._calculate_next_run(send_time)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.info("Сервис отчетов остановлен")
|
||||
raise
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.error("Критическая ошибка в сервисе отчетов: %s", exc)
|
||||
|
||||
def _calculate_next_run(
|
||||
self,
|
||||
send_time: datetime_time,
|
||||
) -> Tuple[datetime, date]:
|
||||
now_msk = datetime.now(self._moscow_tz)
|
||||
candidate = datetime.combine(now_msk.date(), send_time, tzinfo=self._moscow_tz)
|
||||
|
||||
if now_msk >= candidate:
|
||||
candidate += timedelta(days=1)
|
||||
|
||||
report_date = (candidate - timedelta(days=1)).date()
|
||||
return candidate.astimezone(timezone.utc), report_date
|
||||
|
||||
async def _deliver_report(self, report_text: str) -> None:
|
||||
if not self.bot:
|
||||
raise ReportingServiceError("Бот не инициализирован для отправки отчета")
|
||||
|
||||
chat_id = settings.get_reports_chat_id()
|
||||
if not chat_id:
|
||||
raise ReportingServiceError("Не задан чат для отправки отчета")
|
||||
|
||||
topic_id = settings.get_reports_topic_id()
|
||||
|
||||
try:
|
||||
await self.bot.send_message(
|
||||
chat_id=chat_id,
|
||||
text=report_text,
|
||||
message_thread_id=topic_id,
|
||||
)
|
||||
except (TelegramBadRequest, TelegramForbiddenError) as exc:
|
||||
logger.error("Не удалось отправить отчет: %s", exc)
|
||||
raise ReportingServiceError("Не удалось отправить отчет в чат") from exc
|
||||
|
||||
async def _build_report(
|
||||
self,
|
||||
period: ReportPeriod,
|
||||
report_date: Optional[date],
|
||||
) -> str:
|
||||
period_range = self._get_period_range(period, report_date)
|
||||
start_utc = period_range.start_msk.astimezone(timezone.utc).replace(tzinfo=None)
|
||||
end_utc = period_range.end_msk.astimezone(timezone.utc).replace(tzinfo=None)
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
totals = await self._collect_current_totals(session)
|
||||
period_stats = await self._collect_period_stats(session, start_utc, end_utc)
|
||||
|
||||
header = (
|
||||
f"📊 <b>Отчет за {period_range.label}</b>"
|
||||
if period == ReportPeriod.DAILY
|
||||
else f"📊 <b>Отчет за период {period_range.label}</b>"
|
||||
)
|
||||
|
||||
lines = [
|
||||
header,
|
||||
"",
|
||||
"🎯 <b>Триалы</b>",
|
||||
f"• Активных сейчас: {totals['active_trials']}",
|
||||
f"• Новых за период: {period_stats['new_trials']}",
|
||||
"",
|
||||
"💎 <b>Платные подписки</b>",
|
||||
f"• Активных сейчас: {totals['active_paid']}",
|
||||
f"• Новых за период: {period_stats['new_paid_subscriptions']}",
|
||||
"",
|
||||
"💰 <b>Платежи</b>",
|
||||
f"• Оплат подписок: {period_stats['subscription_payments_count']} на сумму "
|
||||
f"{self._format_amount(period_stats['subscription_payments_amount'])}",
|
||||
f"• Пополнений: {period_stats['deposits_count']} на сумму "
|
||||
f"{self._format_amount(period_stats['deposits_amount'])}",
|
||||
f"• Всего поступлений: {period_stats['total_payments_count']} на сумму "
|
||||
f"{self._format_amount(period_stats['total_payments_amount'])}",
|
||||
]
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def _get_period_range(
|
||||
self,
|
||||
period: ReportPeriod,
|
||||
report_date: Optional[date],
|
||||
) -> ReportPeriodRange:
|
||||
now_msk = datetime.now(self._moscow_tz)
|
||||
|
||||
if period == ReportPeriod.DAILY:
|
||||
target_date = report_date or (now_msk.date() - timedelta(days=1))
|
||||
start = datetime.combine(target_date, datetime_time.min, tzinfo=self._moscow_tz)
|
||||
end = start + timedelta(days=1)
|
||||
elif period == ReportPeriod.WEEKLY:
|
||||
end_date = report_date or now_msk.date()
|
||||
start_date = end_date - timedelta(days=7)
|
||||
start = datetime.combine(start_date, datetime_time.min, tzinfo=self._moscow_tz)
|
||||
end = datetime.combine(end_date, datetime_time.min, tzinfo=self._moscow_tz)
|
||||
elif period == ReportPeriod.MONTHLY:
|
||||
end_date = report_date or now_msk.date()
|
||||
start_date = end_date - timedelta(days=30)
|
||||
start = datetime.combine(start_date, datetime_time.min, tzinfo=self._moscow_tz)
|
||||
end = datetime.combine(end_date, datetime_time.min, tzinfo=self._moscow_tz)
|
||||
else: # pragma: no cover - defensive branch
|
||||
raise ReportingServiceError(f"Неизвестный период отчета: {period}")
|
||||
|
||||
label = self._format_period_label(start, end)
|
||||
return ReportPeriodRange(start, end, label)
|
||||
|
||||
async def _collect_current_totals(self, session) -> dict:
|
||||
stats = await get_subscriptions_statistics(session)
|
||||
return {
|
||||
"active_trials": stats.get("trial_subscriptions", 0) or 0,
|
||||
"active_paid": stats.get("paid_subscriptions", 0) or 0,
|
||||
}
|
||||
|
||||
async def _collect_period_stats(
|
||||
self,
|
||||
session,
|
||||
start_utc: datetime,
|
||||
end_utc: datetime,
|
||||
) -> dict:
|
||||
new_trials_result = await session.execute(
|
||||
select(func.count(Subscription.id)).where(
|
||||
Subscription.created_at >= start_utc,
|
||||
Subscription.created_at < end_utc,
|
||||
Subscription.is_trial == True, # noqa: E712
|
||||
)
|
||||
)
|
||||
new_trials = int(new_trials_result.scalar() or 0)
|
||||
|
||||
direct_paid_result = await session.execute(
|
||||
select(func.count(Subscription.id)).where(
|
||||
Subscription.created_at >= start_utc,
|
||||
Subscription.created_at < end_utc,
|
||||
Subscription.is_trial == False, # noqa: E712
|
||||
)
|
||||
)
|
||||
direct_paid = int(direct_paid_result.scalar() or 0)
|
||||
|
||||
conversions_result = await session.execute(
|
||||
select(func.count(SubscriptionConversion.id)).where(
|
||||
SubscriptionConversion.converted_at >= start_utc,
|
||||
SubscriptionConversion.converted_at < end_utc,
|
||||
)
|
||||
)
|
||||
conversions_count = int(conversions_result.scalar() or 0)
|
||||
|
||||
subscription_payments_row = (
|
||||
await session.execute(
|
||||
select(
|
||||
func.count(Transaction.id),
|
||||
func.coalesce(func.sum(Transaction.amount_kopeks), 0),
|
||||
).where(
|
||||
Transaction.type == TransactionType.SUBSCRIPTION_PAYMENT.value,
|
||||
Transaction.is_completed == True, # noqa: E712
|
||||
Transaction.created_at >= start_utc,
|
||||
Transaction.created_at < end_utc,
|
||||
)
|
||||
)
|
||||
).one()
|
||||
|
||||
deposits_row = (
|
||||
await session.execute(
|
||||
select(
|
||||
func.count(Transaction.id),
|
||||
func.coalesce(func.sum(Transaction.amount_kopeks), 0),
|
||||
).where(
|
||||
Transaction.type == TransactionType.DEPOSIT.value,
|
||||
Transaction.is_completed == True, # noqa: E712
|
||||
Transaction.created_at >= start_utc,
|
||||
Transaction.created_at < end_utc,
|
||||
)
|
||||
)
|
||||
).one()
|
||||
|
||||
subscription_payments_count = int(subscription_payments_row[0] or 0)
|
||||
subscription_payments_amount = int(subscription_payments_row[1] or 0)
|
||||
deposits_count = int(deposits_row[0] or 0)
|
||||
deposits_amount = int(deposits_row[1] or 0)
|
||||
|
||||
total_payments_count = subscription_payments_count + deposits_count
|
||||
total_payments_amount = subscription_payments_amount + deposits_amount
|
||||
|
||||
return {
|
||||
"new_trials": new_trials,
|
||||
"new_paid_subscriptions": direct_paid + conversions_count,
|
||||
"subscription_payments_count": subscription_payments_count,
|
||||
"subscription_payments_amount": subscription_payments_amount,
|
||||
"deposits_count": deposits_count,
|
||||
"deposits_amount": deposits_amount,
|
||||
"total_payments_count": total_payments_count,
|
||||
"total_payments_amount": total_payments_amount,
|
||||
}
|
||||
|
||||
def _format_period_label(self, start: datetime, end: datetime) -> str:
|
||||
start_date = start.astimezone(self._moscow_tz).date()
|
||||
end_boundary = (end - timedelta(seconds=1)).astimezone(self._moscow_tz)
|
||||
end_date = end_boundary.date()
|
||||
|
||||
if start_date == end_date:
|
||||
return start_date.strftime("%d.%m.%Y")
|
||||
|
||||
return (
|
||||
f"{start_date.strftime('%d.%m.%Y')} - {end_date.strftime('%d.%m.%Y')}"
|
||||
)
|
||||
|
||||
def _format_amount(self, amount_kopeks: int) -> str:
|
||||
if not amount_kopeks:
|
||||
return "0 ₽"
|
||||
|
||||
rubles = amount_kopeks / 100
|
||||
return f"{rubles:,.2f} ₽".replace(",", " ")
|
||||
|
||||
|
||||
reporting_service = ReportingService()
|
||||
|
||||
20
main.py
20
main.py
@@ -19,6 +19,7 @@ from app.external.yookassa_webhook import start_yookassa_webhook_server
|
||||
from app.external.pal24_webhook import start_pal24_webhook_server, Pal24WebhookServer
|
||||
from app.database.universal_migration import run_universal_migration
|
||||
from app.services.backup_service import backup_service
|
||||
from app.services.reporting_service import reporting_service
|
||||
from app.localization.loader import ensure_locale_templates
|
||||
|
||||
|
||||
@@ -111,7 +112,14 @@ async def main():
|
||||
logger.info("✅ Сервис бекапов инициализирован")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка инициализации сервиса бекапов: {e}")
|
||||
|
||||
|
||||
logger.info("📊 Инициализация сервиса отчетов...")
|
||||
try:
|
||||
reporting_service.set_bot(bot)
|
||||
await reporting_service.start()
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка запуска сервиса отчетов: {e}")
|
||||
|
||||
payment_service = PaymentService(bot)
|
||||
|
||||
webhook_needed = (
|
||||
@@ -188,6 +196,10 @@ async def main():
|
||||
logger.info(f" Мониторинг: {'Включен' if monitoring_task else 'Отключен'}")
|
||||
logger.info(f" Техработы: {'Включен' if maintenance_task else 'Отключен'}")
|
||||
logger.info(f" Проверка версий: {'Включен' if version_check_task else 'Отключен'}")
|
||||
logger.info(
|
||||
" Отчеты: %s",
|
||||
"Включен" if reporting_service.is_running() else "Отключен",
|
||||
)
|
||||
logger.info("=" * 50)
|
||||
|
||||
try:
|
||||
@@ -277,6 +289,12 @@ async def main():
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
logger.info("ℹ️ Остановка сервиса отчетов...")
|
||||
try:
|
||||
await reporting_service.stop()
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка остановки сервиса отчетов: {e}")
|
||||
|
||||
logger.info("ℹ️ Остановка сервиса бекапов...")
|
||||
try:
|
||||
await backup_service.stop_auto_backup()
|
||||
|
||||
Reference in New Issue
Block a user