From 18f65ea493907bb9d0d5f3573f3dc4dbaee5fc87 Mon Sep 17 00:00:00 2001 From: machka-pasla Date: Thu, 7 Aug 2025 23:30:32 +0300 Subject: [PATCH] Refactor notification handling and streamline router registration - Replaced legacy notification functions with a unified NotificationService for better maintainability and clarity. - Updated the main bot router registration to utilize a root router, simplifying the inclusion of user and admin routes. - Removed unused middleware and helper functions to enhance code cleanliness and focus on essential components. - Improved localization by adding new error messages for user interactions. --- bot/handlers/admin/sync_admin.py | 12 +- bot/handlers/user/trial_handler.py | 20 +--- bot/main_bot.py | 67 +---------- bot/middlewares/db_session.py | 40 +++++++ bot/routers.py | 26 +++++ bot/services/notification_service.py | 49 +------- bot/services/subscription_service.py | 85 +++++++------- bot/services/tribute_service.py | 160 +++++++++------------------ db/dal/payment_dal.py | 32 ++++++ db/dal/subscription_dal.py | 23 ++++ db/dal/user_dal.py | 15 +-- locales/en.json | 1 + locales/ru.json | 1 + 13 files changed, 239 insertions(+), 292 deletions(-) create mode 100644 bot/middlewares/db_session.py create mode 100644 bot/routers.py diff --git a/bot/handlers/admin/sync_admin.py b/bot/handlers/admin/sync_admin.py index 0f33482..b0c306c 100644 --- a/bot/handlers/admin/sync_admin.py +++ b/bot/handlers/admin/sync_admin.py @@ -7,7 +7,7 @@ from datetime import datetime, timezone from config.settings import Settings from bot.services.panel_api_service import PanelApiService -from bot.services.notification_service import notify_admin_panel_sync +from bot.services.notification_service import NotificationService from db.dal import user_dal, subscription_dal, panel_sync_dal @@ -287,8 +287,9 @@ async def sync_command_handler( # Send notification to log channel with proper thread handling try: - await notify_admin_panel_sync( - bot, settings, i18n, status, details, + notification_service = NotificationService(bot, settings, i18n) + await notification_service.notify_panel_sync( + status, details, sync_result.get("users_processed", 0), sync_result.get("subs_synced", 0) ) @@ -301,8 +302,9 @@ async def sync_command_handler( # Send notification to log channel about failure try: - await notify_admin_panel_sync( - bot, settings, i18n, "failed", str(e_sync_global), 0, 0 + notification_service = NotificationService(bot, settings, i18n) + await notification_service.notify_panel_sync( + "failed", str(e_sync_global), 0, 0 ) except Exception as e_notification: logging.error(f"Failed to send sync failure notification: {e_notification}") diff --git a/bot/handlers/user/trial_handler.py b/bot/handlers/user/trial_handler.py index d390086..043fb18 100644 --- a/bot/handlers/user/trial_handler.py +++ b/bot/handlers/user/trial_handler.py @@ -7,7 +7,7 @@ from datetime import datetime from config.settings import Settings from bot.services.subscription_service import SubscriptionService from bot.services.panel_api_service import PanelApiService -from bot.services.notification_service import notify_admin_new_trial +from bot.services.notification_service import NotificationService from bot.keyboards.inline.user_keyboards import ( get_trial_confirmation_keyboard, get_main_menu_inline_keyboard, @@ -97,13 +97,8 @@ async def request_trial_confirmation_handler( ) # Send notification to admin about new trial - await notify_admin_new_trial( - callback.bot, - settings, - i18n, - user_id, - end_date_obj, - ) + notification_service = NotificationService(callback.bot, settings, i18n) + await notification_service.notify_trial_activation(user_id, end_date_obj) else: message_key_from_service = ( activation_result.get("message_key", "trial_activation_failed") @@ -264,13 +259,8 @@ async def confirm_activate_trial_handler( ) if activation_result and activation_result.get("activated") and end_date_obj: - await notify_admin_new_trial( - callback.bot, - settings, - i18n, - user_id, - end_date_obj, - ) + notification_service = NotificationService(callback.bot, settings, i18n) + await notification_service.notify_trial_activation(user_id, end_date_obj) @router.callback_query(F.data == "main_action:cancel_trial") diff --git a/bot/main_bot.py b/bot/main_bot.py index 21d2964..1392e01 100644 --- a/bot/main_bot.py +++ b/bot/main_bot.py @@ -1,17 +1,10 @@ import logging import asyncio -from typing import Callable, Dict, Any, Awaitable, Optional +from typing import Dict, Any, Optional -from aiogram import Bot, Dispatcher, BaseMiddleware, Router, F -from aiogram.types import ( - Update, - MenuButtonDefault, - MenuButtonWebApp, - WebAppInfo, - BotCommand, -) +from aiogram import Bot, Dispatcher +from aiogram.types import (MenuButtonDefault, MenuButtonWebApp, WebAppInfo, BotCommand) from aiogram.enums import ParseMode -from aiogram.filters import CommandStart, Command from aiogram.client.default import DefaultBotProperties from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application from aiogram.fsm.storage.memory import MemoryStorage @@ -24,13 +17,11 @@ from config.settings import Settings from db.database_setup import init_db_connection from bot.middlewares.i18n import I18nMiddleware, get_i18n_instance, JsonI18n +from bot.middlewares.db_session import DBSessionMiddleware from bot.middlewares.ban_check_middleware import BanCheckMiddleware from bot.middlewares.action_logger_middleware import ActionLoggerMiddleware -from bot.handlers.user import user_router_aggregate -from bot.handlers.admin import admin_router_aggregate -from bot.handlers import inline_mode -from bot.filters.admin_filter import AdminFilter +from bot.routers import build_root_router from bot.services.yookassa_service import YooKassaService from bot.services.panel_api_service import PanelApiService @@ -46,54 +37,8 @@ from bot.handlers.admin.sync_admin import perform_sync from bot.utils.message_queue import init_queue_manager -class DBSessionMiddleware(BaseMiddleware): - - def __init__(self, async_session_factory: sessionmaker): - super().__init__() - self.async_session_factory = async_session_factory - - async def __call__( - self, - handler: Callable[[Update, Dict[str, Any]], Awaitable[Any]], - event: Update, - data: Dict[str, Any], - ) -> Any: - if self.async_session_factory is None: - logging.critical("DBSessionMiddleware: async_session_factory is None!") - raise RuntimeError( - "async_session_factory not provided to DBSessionMiddleware" - ) - - async with self.async_session_factory() as session: - data["session"] = session - try: - result = await handler(event, data) - - await session.commit() - return result - except Exception: - await session.rollback() - logging.error( - "DBSessionMiddleware: Exception caused rollback.", exc_info=True - ) - raise - - async def register_all_routers(dp: Dispatcher, settings: Settings): - dp.include_router(user_router_aggregate) - - # Add inline mode router (available for all users) - dp.include_router(inline_mode.router) - - admin_main_router = Router(name="admin_main_filtered_router") - admin_filter_instance = AdminFilter(admin_ids=settings.ADMIN_IDS) - - admin_main_router.message.filter(admin_filter_instance) - admin_main_router.callback_query.filter(admin_filter_instance) - - admin_main_router.include_router(admin_router_aggregate) - - dp.include_router(admin_main_router) + dp.include_router(build_root_router(settings)) logging.info("All application routers registered.") diff --git a/bot/middlewares/db_session.py b/bot/middlewares/db_session.py new file mode 100644 index 0000000..006a2d2 --- /dev/null +++ b/bot/middlewares/db_session.py @@ -0,0 +1,40 @@ +import logging +from typing import Callable, Dict, Any, Awaitable + +from aiogram import BaseMiddleware +from aiogram.types import Update +from sqlalchemy.orm import sessionmaker + + +class DBSessionMiddleware(BaseMiddleware): + + def __init__(self, async_session_factory: sessionmaker): + super().__init__() + self.async_session_factory = async_session_factory + + async def __call__( + self, + handler: Callable[[Update, Dict[str, Any]], Awaitable[Any]], + event: Update, + data: Dict[str, Any], + ) -> Any: + if self.async_session_factory is None: + logging.critical("DBSessionMiddleware: async_session_factory is None!") + raise RuntimeError( + "async_session_factory not provided to DBSessionMiddleware" + ) + + async with self.async_session_factory() as session: + data["session"] = session + try: + result = await handler(event, data) + + await session.commit() + return result + except Exception: + await session.rollback() + logging.error( + "DBSessionMiddleware: Exception caused rollback.", exc_info=True + ) + raise + diff --git a/bot/routers.py b/bot/routers.py new file mode 100644 index 0000000..c79b963 --- /dev/null +++ b/bot/routers.py @@ -0,0 +1,26 @@ +from aiogram import Router + +from bot.handlers.user import user_router_aggregate +from bot.handlers import inline_mode +from bot.handlers.admin import admin_router_aggregate +from bot.filters.admin_filter import AdminFilter +from config.settings import Settings + + +def build_root_router(settings: Settings) -> Router: + root = Router(name="root") + + # Public routers + root.include_router(user_router_aggregate) + root.include_router(inline_mode.router) + + # Admin routers behind filter + admin_main_router = Router(name="admin_main_filtered_router") + admin_filter_instance = AdminFilter(admin_ids=settings.ADMIN_IDS) + admin_main_router.message.filter(admin_filter_instance) + admin_main_router.callback_query.filter(admin_filter_instance) + admin_main_router.include_router(admin_router_aggregate) + root.include_router(admin_main_router) + + return root + diff --git a/bot/services/notification_service.py b/bot/services/notification_service.py index 64f02b9..f14cfb3 100644 --- a/bot/services/notification_service.py +++ b/bot/services/notification_service.py @@ -295,51 +295,4 @@ class NotificationService: if to_admins: await self._send_to_admins(message) - -# Legacy functions for backward compatibility -async def notify_admins(bot: Bot, settings: Settings, i18n: JsonI18n, - message_key: str, parse_mode: str | None = None, - **kwargs) -> None: - if not settings.ADMIN_IDS: - return - admin_lang = settings.DEFAULT_LANGUAGE - msg = i18n.gettext(admin_lang, message_key, **kwargs) - for admin_id in settings.ADMIN_IDS: - try: - await bot.send_message(admin_id, msg, parse_mode=parse_mode) - except Exception as e: - logging.error(f"Failed to send admin notification to {admin_id}: {e}") - - -async def notify_admin_new_trial(bot: Bot, settings: Settings, i18n: JsonI18n, - user_id: int, end_date: datetime) -> None: - """Send notification to admins about new trial activation (legacy)""" - notification_service = NotificationService(bot, settings, i18n) - await notification_service.notify_trial_activation(user_id, end_date) - - - - - -async def notify_admin_promo_activation(bot: Bot, settings: Settings, - i18n: JsonI18n, user_id: int, - code: str, - bonus_days: int) -> None: - await notify_admins( - bot, - settings, - i18n, - "admin_promo_activation_notification", - user_id=user_id, - code=code, - bonus_days=bonus_days, - ) - - -async def notify_admin_panel_sync(bot: Bot, settings: Settings, - i18n: JsonI18n, status: str, - details: str, users_processed: int, - subs_synced: int) -> None: - """Send notification to admins about panel sync (legacy)""" - notification_service = NotificationService(bot, settings, i18n) - await notification_service.notify_panel_sync(status, details, users_processed, subs_synced) \ No newline at end of file +# Removed legacy helper functions that duplicated NotificationService API \ No newline at end of file diff --git a/bot/services/subscription_service.py b/bot/services/subscription_service.py index 61795c3..9546175 100644 --- a/bot/services/subscription_service.py +++ b/bot/services/subscription_service.py @@ -34,10 +34,7 @@ class SubscriptionService: else self.settings.DEFAULT_LANGUAGE ) - async def has_had_any_subscription( - self, session: AsyncSession, user_id: int - ) -> bool: - + async def has_had_any_subscription(self, session: AsyncSession, user_id: int) -> bool: return await subscription_dal.has_any_subscription_for_user(session, user_id) async def _notify_admin_panel_user_creation_failed(self, user_id: int): @@ -348,19 +345,12 @@ class SubscriptionService: "message_key": "trial_activation_failed_db", } - panel_update_payload: Dict[str, Any] = { - "uuid": panel_user_uuid, - "expireAt": end_date.isoformat(timespec="milliseconds").replace( - "+00:00", "Z" - ), - "status": "ACTIVE", - "trafficLimitBytes": self.settings.trial_traffic_limit_bytes, - "trafficLimitStrategy": self.settings.USER_TRAFFIC_STRATEGY, - } - if self.settings.parsed_user_squad_uuids: - panel_update_payload["activeInternalSquads"] = ( - self.settings.parsed_user_squad_uuids - ) + panel_update_payload = self._build_panel_update_payload( + panel_user_uuid=panel_user_uuid, + expire_at=end_date, + status="ACTIVE", + traffic_limit_bytes=self.settings.trial_traffic_limit_bytes, + ) updated_panel_user = await self.panel_service.update_user_details_on_panel( panel_user_uuid, panel_update_payload @@ -495,19 +485,12 @@ class SubscriptionService: ) return None - panel_update_payload = { - "uuid": panel_user_uuid, - "expireAt": final_end_date.isoformat(timespec="milliseconds").replace( - "+00:00", "Z" - ), - "status": "ACTIVE", - "trafficLimitBytes": self.settings.user_traffic_limit_bytes, - "trafficLimitStrategy": self.settings.USER_TRAFFIC_STRATEGY, - } - if self.settings.parsed_user_squad_uuids: - panel_update_payload["activeInternalSquads"] = ( - self.settings.parsed_user_squad_uuids - ) + panel_update_payload = self._build_panel_update_payload( + panel_user_uuid=panel_user_uuid, + expire_at=final_end_date, + status="ACTIVE", + traffic_limit_bytes=self.settings.user_traffic_limit_bytes, + ) updated_panel_user = await self.panel_service.update_user_details_on_panel( panel_user_uuid, panel_update_payload @@ -598,17 +581,13 @@ class SubscriptionService: if updated_sub_model: # Prepare panel update payload - panel_update_payload = { - "expireAt": new_end_date_obj.isoformat( - timespec="milliseconds" - ).replace("+00:00", "Z") - } - - # For promo code activations, remove traffic limit - if "promo code" in reason.lower(): - panel_update_payload["trafficLimitBytes"] = self.settings.user_traffic_limit_bytes - panel_update_payload["trafficLimitStrategy"] = self.settings.USER_TRAFFIC_STRATEGY - logging.info(f"Updating traffic limit for user {user_id} to {self.settings.user_traffic_limit_bytes} bytes due to promo code activation") + panel_update_payload = self._build_panel_update_payload( + expire_at=new_end_date_obj, + traffic_limit_bytes=( + self.settings.user_traffic_limit_bytes if "promo code" in reason.lower() else None + ), + include_uuid=False, + ) panel_update_success = ( await self.panel_service.update_user_details_on_panel( @@ -775,3 +754,27 @@ class SubscriptionService: logging.warning( f"Could not find subscription for user {user_id} ending at {subscription_end_date.isoformat()} to update notification time." ) + + # Helpers + def _build_panel_update_payload( + self, + *, + panel_user_uuid: Optional[str] = None, + expire_at: Optional[datetime] = None, + status: Optional[str] = None, + traffic_limit_bytes: Optional[int] = None, + include_uuid: bool = True, + ) -> Dict[str, Any]: + payload: Dict[str, Any] = {} + if include_uuid and panel_user_uuid: + payload["uuid"] = panel_user_uuid + if expire_at is not None: + payload["expireAt"] = expire_at.isoformat(timespec="milliseconds").replace("+00:00", "Z") + if status is not None: + payload["status"] = status + if traffic_limit_bytes is not None: + payload["trafficLimitBytes"] = traffic_limit_bytes + payload["trafficLimitStrategy"] = self.settings.USER_TRAFFIC_STRATEGY + if self.settings.parsed_user_squad_uuids: + payload["activeInternalSquads"] = self.settings.parsed_user_squad_uuids + return payload diff --git a/bot/services/tribute_service.py b/bot/services/tribute_service.py index 6ea1ae3..5401922 100644 --- a/bot/services/tribute_service.py +++ b/bot/services/tribute_service.py @@ -39,11 +39,16 @@ def convert_period_to_months(period: Optional[str]) -> int: class TributeService: - def __init__(self, bot: Bot, settings: Settings, i18n: JsonI18n, - async_session_factory: sessionmaker, - panel_service: PanelApiService, - subscription_service: SubscriptionService, - referral_service: ReferralService): + def __init__( + self, + bot: Bot, + settings: Settings, + i18n: JsonI18n, + async_session_factory: sessionmaker, + panel_service: PanelApiService, + subscription_service: SubscriptionService, + referral_service: ReferralService, + ): self.bot = bot self.settings = settings self.i18n = i18n @@ -52,8 +57,7 @@ class TributeService: self.subscription_service = subscription_service self.referral_service = referral_service - async def handle_webhook(self, raw_body: bytes, - signature_header: Optional[str]) -> web.Response: + async def handle_webhook(self, raw_body: bytes, signature_header: Optional[str]) -> web.Response: settings = self.settings bot = self.bot i18n = self.i18n @@ -79,96 +83,53 @@ class TributeService: json.dumps(payload, ensure_ascii=False), ) - event_name = payload.get('name') - data = payload.get('payload', {}) - user_id = data.get('telegram_user_id') - price_val = ( - data.get('amount') - or data.get('amount_paid') - or data.get('price') - ) + # Tribute webhook spec: only two events are sent + # name: new_subscription | cancelled_subscription + event_name = payload.get("name") + data = payload.get("payload", {}) - if not user_id or price_val is None: - return web.Response(status=200, text="ok_missing_fields") + # Mandatory routing fields + user_id = data.get("telegram_user_id") + if not user_id: + return web.Response(status=400, text="missing_telegram_user_id") - period_val = data.get('period') + period_val = data.get("period") months = convert_period_to_months(period_val) - price_rub = price_val / 100 + + # Price/amount from spec is integer cents in currency; we store float in Payment + amount_value = data.get("amount") or data.get("price") + currency = (data.get("currency") or settings.DEFAULT_CURRENCY_SYMBOL or "RUB").upper() + amount_float = float(amount_value) if amount_value is not None else 0.0 async with async_session_factory() as session: - # Normalize provider payment identifier to be unique per successful charge - # Prefer true payment/transaction identifiers over subscription id - provider_payment_id = ( - data.get('payment_id') - or data.get('invoice_id') - or data.get('order_id') - or data.get('transaction_id') - or data.get('charge_id') - or data.get('subscription_payment_id') - ) - if provider_payment_id is None: - # Fallback to subscription_id which may be stable across renewals - # To avoid deduplicating different renewals under same subscription, - # append a timestamp if available - base_sub_id = data.get('subscription_id') - paid_at = ( - data.get('paid_at') - or data.get('created_at') - or payload.get('timestamp') - or payload.get('id') + if event_name == "new_subscription": + # Build a stable provider payment id from subscription and timestamps + provider_payment_id = str(data.get("subscription_id")) + # Idempotent ensure payment + payment_record = await payment_dal.ensure_payment_with_provider_id( + session, + user_id=int(user_id), + amount=amount_float, + currency=currency, + months=months, + description="Tribute subscription", + provider="tribute", + provider_payment_id=provider_payment_id, ) - if base_sub_id is not None and paid_at is not None: - provider_payment_id = f"{base_sub_id}:{paid_at}" - elif base_sub_id is not None: - provider_payment_id = str(base_sub_id) - else: - provider_payment_id = str(provider_payment_id) - # Consider multiple Tribute events as successful charge events - success_events = { - 'new_subscription', - 'payment_succeeded', - 'subscription_renewed', - 'subscription_payment_succeeded', - 'invoice_paid', - } - - if event_name in success_events: - existing_payment = await payment_dal.get_payment_by_provider_payment_id( - session, provider_payment_id) - if existing_payment: - logging.info( - "Duplicate Tribute payment webhook ignored for provider_payment_id %s", - provider_payment_id, - ) - payment_record = existing_payment - else: - payment_record = await payment_dal.create_payment_record( - session, - { - 'user_id': user_id, - 'amount': float(price_rub), - 'currency': 'RUB', - 'status': 'succeeded', - 'description': 'Tribute subscription', - 'subscription_duration_months': months, - 'provider_payment_id': provider_payment_id, - 'provider': 'tribute', - }, - ) activation_details = await subscription_service.activate_subscription( session, - user_id, + int(user_id), months, - float(price_rub), + float(amount_float), payment_record.payment_id, - provider='tribute', + provider="tribute", ) referral_bonus = await referral_service.apply_referral_bonuses_for_payment( - session, user_id, months) + session, int(user_id), months) await session.commit() - db_user = await user_dal.get_user_by_id(session, user_id) + db_user = await user_dal.get_user_by_id(session, int(user_id)) lang = db_user.language_code if db_user and db_user.language_code else settings.DEFAULT_LANGUAGE _ = lambda k, **kw: i18n.gettext(lang, k, **kw) @@ -213,7 +174,7 @@ class TributeService: try: await bot.send_message( - user_id, + int(user_id), success_msg, reply_markup=markup, parse_mode="HTML", @@ -226,21 +187,19 @@ class TributeService: # Send notification about payment try: notification_service = NotificationService(bot, settings, i18n) - user = await user_dal.get_user_by_id(session, user_id) + user = await user_dal.get_user_by_id(session, int(user_id)) await notification_service.notify_payment_received( - user_id=user_id, - amount=float(price_rub), - currency="RUB", + user_id=int(user_id), + amount=float(amount_float), + currency=currency, months=months, payment_provider="tribute", username=user.username if user else None ) except Exception as e: logging.error(f"Failed to send tribute payment notification: {e}") - - elif event_name == 'subscription_cancelled': - # Handle tribute subscription cancellation - await self._handle_tribute_cancellation(session, user_id, bot, i18n) + elif event_name == "cancelled_subscription": + await self._handle_tribute_cancellation(session, int(user_id), bot, i18n) else: await session.commit() @@ -254,22 +213,7 @@ class TributeService: try: # Set all user's subscriptions to expire in 1 day (grace period) - grace_end_date = datetime.now(timezone.utc) + timedelta(days=1) - - # Get all active subscriptions for the user - user_subs = await subscription_dal.get_active_subscriptions_for_user(session, user_id) - - for sub in user_subs: - await subscription_dal.update_subscription( - session, - sub.subscription_id, - { - 'end_date': grace_end_date, - 'status_from_panel': 'CANCELLED', - 'skip_notifications': True # Skip future notifications for cancelled subs - } - ) - + await subscription_dal.set_user_subscriptions_cancelled_with_grace(session, user_id, grace_days=1) await session.commit() # Send notification about cancellation if enabled @@ -292,7 +236,7 @@ class TributeService: try: await bot.send_message( - user_id, + int(user_id), cancellation_msg, reply_markup=markup, parse_mode="HTML" diff --git a/db/dal/payment_dal.py b/db/dal/payment_dal.py index 9bb2c12..a52b825 100644 --- a/db/dal/payment_dal.py +++ b/db/dal/payment_dal.py @@ -47,6 +47,38 @@ async def get_payment_by_provider_payment_id( return result.scalar_one_or_none() +async def ensure_payment_with_provider_id( + session: AsyncSession, + *, + user_id: int, + amount: float, + currency: str, + months: int, + description: str, + provider: str, + provider_payment_id: str) -> Payment: + """Idempotently create a payment record for a provider event. + + If a payment with the same provider_payment_id already exists, returns it. + Otherwise creates a new succeeded payment with provided data. + """ + existing = await get_payment_by_provider_payment_id(session, provider_payment_id) + if existing: + return existing + + payment_payload: Dict[str, Any] = { + "user_id": user_id, + "amount": float(amount), + "currency": currency, + "status": "succeeded", + "description": description, + "subscription_duration_months": months, + "provider_payment_id": provider_payment_id, + "provider": provider, + } + return await create_payment_record(session, payment_payload) + + async def get_payment_by_db_id(session: AsyncSession, payment_db_id: int) -> Optional[Payment]: diff --git a/db/dal/subscription_dal.py b/db/dal/subscription_dal.py index 91ede4a..6a0b0e6 100644 --- a/db/dal/subscription_dal.py +++ b/db/dal/subscription_dal.py @@ -53,6 +53,29 @@ async def update_subscription( return sub +async def set_user_subscriptions_cancelled_with_grace( + session: AsyncSession, user_id: int, grace_days: int = 1) -> int: + """Mark all active user subscriptions as cancelled with a short grace period. + + Sets end_date to now + grace_days, status_from_panel to 'CANCELLED', and + skip future notifications to reduce noise after cancellation. + Returns number of updated rows. + """ + from datetime import datetime, timezone, timedelta + grace_end = datetime.now(timezone.utc) + timedelta(days=grace_days) + stmt = ( + update(Subscription) + .where(Subscription.user_id == user_id, Subscription.is_active == True) + .values( + end_date=grace_end, + status_from_panel="CANCELLED", + skip_notifications=True, + ) + ) + result = await session.execute(stmt) + return result.rowcount or 0 + + async def upsert_subscription(session: AsyncSession, sub_payload: Dict[str, Any]) -> Subscription: panel_sub_uuid = sub_payload.get("panel_subscription_uuid") diff --git a/db/dal/user_dal.py b/db/dal/user_dal.py index af8f58c..40854eb 100644 --- a/db/dal/user_dal.py +++ b/db/dal/user_dal.py @@ -30,20 +30,7 @@ async def get_user_by_panel_uuid( return result.scalar_one_or_none() -async def get_user( - session: AsyncSession, - *, - user_id: Optional[int] = None, - username: Optional[str] = None, - panel_uuid: Optional[str] = None, -) -> Optional[User]: - if user_id is not None: - return await get_user_by_id(session, user_id) - if username is not None: - return await get_user_by_username(session, username) - if panel_uuid is not None: - return await get_user_by_panel_uuid(session, panel_uuid) - return None +## Removed unused generic get_user helper to keep DAL explicit and simple async def create_user(session: AsyncSession, user_data: Dict[str, Any]) -> User: diff --git a/locales/en.json b/locales/en.json index 5712a5f..a7e77f4 100644 --- a/locales/en.json +++ b/locales/en.json @@ -16,6 +16,7 @@ "choose_language": "Choose language / Выберите язык:", "language_set_alert": "Language changed!", "error_occurred_try_again": "An error occurred, please try again.", + "error_try_again": "Please try again.", "error_displaying_menu": "Error displaying menu.", "main_menu_unknown_action": "Unknown action.", diff --git a/locales/ru.json b/locales/ru.json index 8197e9a..34063e2 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -16,6 +16,7 @@ "choose_language": "Выберите язык / Select language:", "language_set_alert": "Язык изменен!", "error_occurred_try_again": "Произошла ошибка, попробуйте снова.", + "error_try_again": "Попробуйте еще раз.", "error_displaying_menu": "Ошибка отображения меню.", "main_menu_unknown_action": "Неизвестное действие.",