chore: run lint and prettifier
This commit is contained in:
@@ -1,11 +1,9 @@
|
||||
from aiogram import Router
|
||||
|
||||
from . import start
|
||||
from . import promo_user, referral, start, trial_handler
|
||||
|
||||
# TODO: after splitting subscription into a package, replace this import
|
||||
from .subscription import router as subscription_router
|
||||
from . import referral
|
||||
from . import promo_user
|
||||
from . import trial_handler
|
||||
|
||||
user_router_aggregate = Router(name="user_router_aggregate")
|
||||
|
||||
|
||||
+250
-168
@@ -1,37 +1,34 @@
|
||||
import logging
|
||||
import json
|
||||
import asyncio
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from typing import Optional, Dict, Any
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
from aiohttp import web
|
||||
from aiogram import Bot
|
||||
from aiohttp import web
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from yookassa.domain.notification import WebhookNotification
|
||||
from yookassa.domain.models.amount import Amount as YooKassaAmount
|
||||
|
||||
from db.dal import payment_dal, user_dal, user_billing_dal
|
||||
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
from bot.services.referral_service import ReferralService
|
||||
from bot.services.panel_api_service import PanelApiService
|
||||
from bot.services.yookassa_service import YooKassaService
|
||||
from bot.services.lknpd_service import LknpdService
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
from config.settings import Settings
|
||||
from bot.services.notification_service import NotificationService
|
||||
from bot.keyboards.inline.user_keyboards import get_connect_and_main_keyboard
|
||||
from bot.utils.text_sanitizer import sanitize_display_name, username_for_display
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
from bot.services.lknpd_service import LknpdService
|
||||
from bot.services.notification_service import NotificationService
|
||||
from bot.services.panel_api_service import PanelApiService
|
||||
from bot.services.referral_service import ReferralService
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
from bot.services.yookassa_service import YooKassaService
|
||||
from bot.utils.config_link import prepare_config_links
|
||||
from bot.utils.request_security import ip_in_allowlist, request_client_ip
|
||||
from bot.utils.text_sanitizer import sanitize_display_name, username_for_display
|
||||
from config.settings import Settings
|
||||
from db.dal import payment_dal, user_billing_dal, user_dal
|
||||
|
||||
payment_processing_lock = asyncio.Lock()
|
||||
|
||||
YOOKASSA_EVENT_PAYMENT_SUCCEEDED = 'payment.succeeded'
|
||||
YOOKASSA_EVENT_PAYMENT_CANCELED = 'payment.canceled'
|
||||
YOOKASSA_EVENT_PAYMENT_WAITING_FOR_CAPTURE = 'payment.waiting_for_capture'
|
||||
YOOKASSA_EVENT_PAYMENT_SUCCEEDED = "payment.succeeded"
|
||||
YOOKASSA_EVENT_PAYMENT_CANCELED = "payment.canceled"
|
||||
YOOKASSA_EVENT_PAYMENT_WAITING_FOR_CAPTURE = "payment.waiting_for_capture"
|
||||
YOOKASSA_WEBHOOK_ALLOWED_IPS = [
|
||||
"185.71.76.0/27",
|
||||
"185.71.77.0/27",
|
||||
@@ -43,23 +40,28 @@ YOOKASSA_WEBHOOK_ALLOWED_IPS = [
|
||||
]
|
||||
|
||||
|
||||
async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
payment_info_from_webhook: dict,
|
||||
i18n: JsonI18n, settings: Settings,
|
||||
panel_service: PanelApiService,
|
||||
subscription_service: SubscriptionService,
|
||||
referral_service: ReferralService,
|
||||
lknpd_service: Optional[LknpdService] = None):
|
||||
async def process_successful_payment(
|
||||
session: AsyncSession,
|
||||
bot: Bot,
|
||||
payment_info_from_webhook: dict,
|
||||
i18n: JsonI18n,
|
||||
settings: Settings,
|
||||
panel_service: PanelApiService,
|
||||
subscription_service: SubscriptionService,
|
||||
referral_service: ReferralService,
|
||||
lknpd_service: Optional[LknpdService] = None,
|
||||
):
|
||||
metadata = payment_info_from_webhook.get("metadata", {})
|
||||
user_id_str = metadata.get("user_id")
|
||||
subscription_months_str = metadata.get("subscription_months")
|
||||
traffic_gb_str = metadata.get("traffic_gb")
|
||||
sale_mode = metadata.get("sale_mode") or ("traffic" if settings.traffic_sale_mode else "subscription")
|
||||
sale_mode = metadata.get("sale_mode") or (
|
||||
"traffic" if settings.traffic_sale_mode else "subscription"
|
||||
)
|
||||
sale_mode_base = sale_mode.split("@", 1)[0].split("|", 1)[0]
|
||||
promo_code_id_str = metadata.get("promo_code_id")
|
||||
payment_db_id_str = metadata.get("payment_db_id")
|
||||
auto_renew_subscription_id_str = metadata.get(
|
||||
"auto_renew_for_subscription_id")
|
||||
auto_renew_subscription_id_str = metadata.get("auto_renew_for_subscription_id")
|
||||
|
||||
# For auto-renew payments, payment_db_id may be absent. In that case,
|
||||
# we will create/ensure a payment record idempotently using provider payment id.
|
||||
@@ -78,12 +80,17 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
user_id = int(user_id_str)
|
||||
subscription_months = float(subscription_months_str or 0)
|
||||
traffic_amount_gb = float(traffic_gb_str) if traffic_gb_str else subscription_months
|
||||
payment_db_id = int(
|
||||
payment_db_id_str) if payment_db_id_str and payment_db_id_str.isdigit() else None
|
||||
is_auto_renew = bool(auto_renew_subscription_id_str and not payment_db_id and sale_mode_base == "subscription")
|
||||
promo_code_id = int(
|
||||
promo_code_id_str
|
||||
) if promo_code_id_str and promo_code_id_str.isdigit() else None
|
||||
payment_db_id = (
|
||||
int(payment_db_id_str) if payment_db_id_str and payment_db_id_str.isdigit() else None
|
||||
)
|
||||
is_auto_renew = bool(
|
||||
auto_renew_subscription_id_str
|
||||
and not payment_db_id
|
||||
and sale_mode_base == "subscription"
|
||||
)
|
||||
promo_code_id = (
|
||||
int(promo_code_id_str) if promo_code_id_str and promo_code_id_str.isdigit() else None
|
||||
)
|
||||
|
||||
amount_data = payment_info_from_webhook.get("amount", {})
|
||||
months_for_record = int(subscription_months) if sale_mode_base == "subscription" else 0
|
||||
@@ -100,6 +107,7 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
)
|
||||
return
|
||||
from db.dal import payment_dal as _payment_dal
|
||||
|
||||
payment_record = await _payment_dal.get_payment_by_provider_payment_id(
|
||||
session, yk_payment_id_from_hook
|
||||
)
|
||||
@@ -110,8 +118,8 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
amount=payment_value,
|
||||
currency=amount_data.get("currency", settings.DEFAULT_CURRENCY_SYMBOL),
|
||||
months=months_for_record or 1,
|
||||
description=payment_info_from_webhook.get(
|
||||
"description") or f"Auto-renewal for {months_for_record or subscription_months} months",
|
||||
description=payment_info_from_webhook.get("description")
|
||||
or f"Auto-renewal for {months_for_record or subscription_months} months",
|
||||
provider="yookassa",
|
||||
provider_payment_id=yk_payment_id_from_hook,
|
||||
)
|
||||
@@ -143,25 +151,24 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
)
|
||||
|
||||
await payment_dal.update_payment_status_by_db_id(
|
||||
session, payment_db_id, "failed_user_not_found",
|
||||
payment_info_from_webhook.get("id"))
|
||||
session, payment_db_id, "failed_user_not_found", payment_info_from_webhook.get("id")
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
except (TypeError, ValueError) as e:
|
||||
logging.error(
|
||||
f"Invalid metadata format for payment processing: {metadata} - {e}"
|
||||
)
|
||||
logging.error(f"Invalid metadata format for payment processing: {metadata} - {e}")
|
||||
|
||||
if payment_db_id_str and payment_db_id_str.isdigit():
|
||||
try:
|
||||
await payment_dal.update_payment_status_by_db_id(
|
||||
session, int(payment_db_id_str), "failed_metadata_error",
|
||||
payment_info_from_webhook.get("id"))
|
||||
except Exception as e_upd:
|
||||
logging.error(
|
||||
f"Failed to update payment status after metadata error: {e_upd}"
|
||||
session,
|
||||
int(payment_db_id_str),
|
||||
"failed_metadata_error",
|
||||
payment_info_from_webhook.get("id"),
|
||||
)
|
||||
except Exception as e_upd:
|
||||
logging.error(f"Failed to update payment status after metadata error: {e_upd}")
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -183,12 +190,18 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
# Try to capture and save payment method for future charges if available
|
||||
try:
|
||||
payment_method = payment_info_from_webhook.get("payment_method")
|
||||
if settings.yookassa_autopayments_active and isinstance(payment_method, dict) and payment_method.get("saved", False):
|
||||
if (
|
||||
settings.yookassa_autopayments_active
|
||||
and isinstance(payment_method, dict)
|
||||
and payment_method.get("saved", False)
|
||||
):
|
||||
pm_id = payment_method.get("id")
|
||||
pm_type = payment_method.get("type")
|
||||
title = payment_method.get("title")
|
||||
card = payment_method.get("card") or {}
|
||||
account_number = payment_method.get("account_number") or payment_method.get("account")
|
||||
account_number = payment_method.get("account_number") or payment_method.get(
|
||||
"account"
|
||||
)
|
||||
display_network = None
|
||||
display_last4 = None
|
||||
# Build generic display for various instrument types
|
||||
@@ -228,7 +241,9 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
logging.exception("Failed to persist multi-card YooKassa method from webhook")
|
||||
except Exception:
|
||||
logging.exception("Failed to persist YooKassa payment method from webhook")
|
||||
months_for_activation = int(subscription_months) if sale_mode_base == "subscription" else int(traffic_amount_gb)
|
||||
months_for_activation = (
|
||||
int(subscription_months) if sale_mode_base == "subscription" else int(traffic_amount_gb)
|
||||
)
|
||||
activation_details = await subscription_service.activate_subscription(
|
||||
session,
|
||||
user_id,
|
||||
@@ -238,32 +253,32 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
promo_code_id_from_payment=promo_code_id,
|
||||
provider="yookassa",
|
||||
sale_mode=sale_mode,
|
||||
traffic_gb=traffic_amount_gb if sale_mode_base in {"traffic", "traffic_package", "topup", "premium_topup"} else None,
|
||||
traffic_gb=traffic_amount_gb
|
||||
if sale_mode_base in {"traffic", "traffic_package", "topup", "premium_topup"}
|
||||
else None,
|
||||
)
|
||||
|
||||
if not activation_details or not activation_details.get('end_date'):
|
||||
if not activation_details or not activation_details.get("end_date"):
|
||||
logging.error(
|
||||
f"Failed to activate subscription for user {user_id} after payment {yk_payment_id_from_hook}"
|
||||
)
|
||||
raise Exception(
|
||||
f"Subscription Error: Failed to activate for user {user_id}")
|
||||
raise Exception(f"Subscription Error: Failed to activate for user {user_id}")
|
||||
|
||||
updated_payment_record = await payment_dal.update_payment_status_by_db_id(
|
||||
session,
|
||||
payment_db_id=payment_db_id,
|
||||
new_status=payment_info_from_webhook.get("status", "succeeded"),
|
||||
yk_payment_id=yk_payment_id_from_hook)
|
||||
yk_payment_id=yk_payment_id_from_hook,
|
||||
)
|
||||
if not updated_payment_record:
|
||||
logging.error(
|
||||
f"Failed to update payment record {payment_db_id} for yk_id {yk_payment_id_from_hook}"
|
||||
)
|
||||
raise Exception(
|
||||
f"DB Error: Could not update payment record {payment_db_id}")
|
||||
raise Exception(f"DB Error: Could not update payment record {payment_db_id}")
|
||||
|
||||
base_subscription_end_date = activation_details['end_date']
|
||||
base_subscription_end_date = activation_details["end_date"]
|
||||
final_end_date_for_user = base_subscription_end_date
|
||||
applied_promo_bonus_days = activation_details.get(
|
||||
"applied_promo_bonus_days", 0)
|
||||
applied_promo_bonus_days = activation_details.get("applied_promo_bonus_days", 0)
|
||||
|
||||
referral_bonus_info = None
|
||||
if sale_mode_base == "subscription":
|
||||
@@ -275,19 +290,24 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
skip_if_active_before_payment=False,
|
||||
)
|
||||
applied_referee_bonus_days_from_referral: Optional[int] = None
|
||||
if referral_bonus_info and referral_bonus_info.get(
|
||||
"referee_new_end_date"):
|
||||
final_end_date_for_user = referral_bonus_info[
|
||||
"referee_new_end_date"]
|
||||
if referral_bonus_info and referral_bonus_info.get("referee_new_end_date"):
|
||||
final_end_date_for_user = referral_bonus_info["referee_new_end_date"]
|
||||
applied_referee_bonus_days_from_referral = referral_bonus_info.get(
|
||||
"referee_bonus_applied_days")
|
||||
"referee_bonus_applied_days"
|
||||
)
|
||||
|
||||
# Use user's DB language for all user-facing messages
|
||||
user_lang = db_user.language_code if db_user and db_user.language_code else settings.DEFAULT_LANGUAGE
|
||||
user_lang = (
|
||||
db_user.language_code
|
||||
if db_user and db_user.language_code
|
||||
else settings.DEFAULT_LANGUAGE
|
||||
)
|
||||
_ = lambda key, **kwargs: i18n.gettext(user_lang, key, **kwargs)
|
||||
|
||||
traffic_label = (
|
||||
str(int(traffic_amount_gb)) if float(traffic_amount_gb).is_integer() else f"{traffic_amount_gb:g}"
|
||||
str(int(traffic_amount_gb))
|
||||
if float(traffic_amount_gb).is_integer()
|
||||
else f"{traffic_amount_gb:g}"
|
||||
)
|
||||
if should_send_lknpd_receipt:
|
||||
receipt_item_name = payment_info_from_webhook.get("description")
|
||||
@@ -295,7 +315,9 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
if sale_mode_base in {"traffic", "traffic_package", "topup", "premium_topup"}:
|
||||
receipt_item_name = settings.LKNPD_RECEIPT_NAME_TRAFFIC.format(gb=traffic_label)
|
||||
else:
|
||||
receipt_item_name = settings.LKNPD_RECEIPT_NAME_SUBSCRIPTION.format(months=int(subscription_months))
|
||||
receipt_item_name = settings.LKNPD_RECEIPT_NAME_SUBSCRIPTION.format(
|
||||
months=int(subscription_months)
|
||||
)
|
||||
try:
|
||||
await lknpd_service.create_income_receipt(
|
||||
item_name=receipt_item_name,
|
||||
@@ -317,14 +339,16 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
details_message = _(
|
||||
"yookassa_auto_renewal",
|
||||
months=int(subscription_months),
|
||||
end_date=final_end_date_for_user.strftime('%Y-%m-%d'),
|
||||
end_date=final_end_date_for_user.strftime("%Y-%m-%d"),
|
||||
)
|
||||
details_markup = None
|
||||
elif sale_mode_base in {"traffic", "traffic_package", "topup", "premium_topup"}:
|
||||
details_message = _(
|
||||
"payment_successful_traffic_full",
|
||||
traffic_gb=traffic_label,
|
||||
end_date=final_end_date_for_user.strftime('%Y-%m-%d') if final_end_date_for_user else "—",
|
||||
end_date=final_end_date_for_user.strftime("%Y-%m-%d")
|
||||
if final_end_date_for_user
|
||||
else "—",
|
||||
config_link=config_link_text,
|
||||
)
|
||||
details_markup = get_connect_and_main_keyboard(
|
||||
@@ -339,21 +363,26 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
if applied_referee_bonus_days_from_referral and final_end_date_for_user:
|
||||
inviter_name_display = _("friend_placeholder")
|
||||
if db_user and db_user.referred_by_id:
|
||||
inviter = await user_dal.get_user_by_id(
|
||||
session, db_user.referred_by_id)
|
||||
inviter = await user_dal.get_user_by_id(session, db_user.referred_by_id)
|
||||
if inviter:
|
||||
safe_name = sanitize_display_name(inviter.first_name) if inviter.first_name else None
|
||||
safe_name = (
|
||||
sanitize_display_name(inviter.first_name)
|
||||
if inviter.first_name
|
||||
else None
|
||||
)
|
||||
if safe_name:
|
||||
inviter_name_display = safe_name
|
||||
elif inviter.username:
|
||||
inviter_name_display = username_for_display(inviter.username, with_at=False)
|
||||
inviter_name_display = username_for_display(
|
||||
inviter.username, with_at=False
|
||||
)
|
||||
|
||||
details_message = _(
|
||||
"payment_successful_with_referral_bonus_full",
|
||||
months=int(subscription_months),
|
||||
base_end_date=base_subscription_end_date.strftime('%Y-%m-%d'),
|
||||
base_end_date=base_subscription_end_date.strftime("%Y-%m-%d"),
|
||||
bonus_days=applied_referee_bonus_days_from_referral,
|
||||
final_end_date=final_end_date_for_user.strftime('%Y-%m-%d'),
|
||||
final_end_date=final_end_date_for_user.strftime("%Y-%m-%d"),
|
||||
inviter_name=inviter_name_display,
|
||||
config_link=config_link_text,
|
||||
)
|
||||
@@ -362,14 +391,14 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
"payment_successful_with_promo_full",
|
||||
months=int(subscription_months),
|
||||
bonus_days=applied_promo_bonus_days,
|
||||
end_date=final_end_date_for_user.strftime('%Y-%m-%d'),
|
||||
end_date=final_end_date_for_user.strftime("%Y-%m-%d"),
|
||||
config_link=config_link_text,
|
||||
)
|
||||
elif final_end_date_for_user:
|
||||
details_message = _(
|
||||
"payment_successful_full",
|
||||
months=int(subscription_months),
|
||||
end_date=final_end_date_for_user.strftime('%Y-%m-%d'),
|
||||
end_date=final_end_date_for_user.strftime("%Y-%m-%d"),
|
||||
config_link=config_link_text,
|
||||
)
|
||||
else:
|
||||
@@ -395,9 +424,7 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
except Exception as e_notify:
|
||||
logging.error(
|
||||
f"Failed to send payment details message to user {user_id}: {e_notify}"
|
||||
)
|
||||
logging.error(f"Failed to send payment details message to user {user_id}: {e_notify}")
|
||||
|
||||
# Send notification about payment
|
||||
try:
|
||||
@@ -417,7 +444,9 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
months=int(subscription_months) if sale_mode_base == "subscription" else 0,
|
||||
payment_provider="yookassa", # This is specifically for YooKassa webhook
|
||||
username=user.username if user else None,
|
||||
traffic_gb=traffic_amount_gb if sale_mode_base in {"traffic", "traffic_package", "topup", "premium_topup"} else None,
|
||||
traffic_gb=traffic_amount_gb
|
||||
if sale_mode_base in {"traffic", "traffic_package", "topup", "premium_topup"}
|
||||
else None,
|
||||
traffic_is_premium=sale_mode_base == "premium_topup",
|
||||
tariff_key=tariff_for_log,
|
||||
)
|
||||
@@ -427,14 +456,19 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
except Exception as e_process:
|
||||
logging.error(
|
||||
f"Error during process_successful_payment main try block for user {user_id}: {e_process}",
|
||||
exc_info=True)
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
raise
|
||||
|
||||
|
||||
async def process_cancelled_payment(session: AsyncSession, bot: Bot,
|
||||
payment_info_from_webhook: dict,
|
||||
i18n: JsonI18n, settings: Settings):
|
||||
async def process_cancelled_payment(
|
||||
session: AsyncSession,
|
||||
bot: Bot,
|
||||
payment_info_from_webhook: dict,
|
||||
i18n: JsonI18n,
|
||||
settings: Settings,
|
||||
):
|
||||
|
||||
metadata = payment_info_from_webhook.get("metadata", {})
|
||||
user_id_str = metadata.get("user_id")
|
||||
@@ -449,8 +483,7 @@ async def process_cancelled_payment(session: AsyncSession, bot: Bot,
|
||||
user_id = int(user_id_str)
|
||||
payment_db_id = int(payment_db_id_str)
|
||||
except ValueError:
|
||||
logging.error(
|
||||
f"Invalid metadata in cancelled payment webhook: {metadata}")
|
||||
logging.error(f"Invalid metadata in cancelled payment webhook: {metadata}")
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -458,7 +491,8 @@ async def process_cancelled_payment(session: AsyncSession, bot: Bot,
|
||||
session,
|
||||
payment_db_id=payment_db_id,
|
||||
new_status=payment_info_from_webhook.get("status", "canceled"),
|
||||
yk_payment_id=payment_info_from_webhook.get("id"))
|
||||
yk_payment_id=payment_info_from_webhook.get("id"),
|
||||
)
|
||||
|
||||
if updated_payment:
|
||||
logging.info(
|
||||
@@ -471,7 +505,8 @@ async def process_cancelled_payment(session: AsyncSession, bot: Bot,
|
||||
|
||||
db_user = await user_dal.get_user_by_id(session, user_id)
|
||||
user_lang = settings.DEFAULT_LANGUAGE
|
||||
if db_user and db_user.language_code: user_lang = db_user.language_code
|
||||
if db_user and db_user.language_code:
|
||||
user_lang = db_user.language_code
|
||||
|
||||
_ = lambda key, **kwargs: i18n.gettext(user_lang, key, **kwargs)
|
||||
await bot.send_message(user_id, _("payment_failed"))
|
||||
@@ -479,29 +514,25 @@ async def process_cancelled_payment(session: AsyncSession, bot: Bot,
|
||||
except Exception as e_process_cancel:
|
||||
logging.error(
|
||||
f"Error processing cancelled payment for user {user_id}, payment_db_id {payment_db_id}: {e_process_cancel}",
|
||||
exc_info=True)
|
||||
exc_info=True,
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
async def yookassa_webhook_route(request: web.Request):
|
||||
|
||||
try:
|
||||
bot: Bot = request.app['bot']
|
||||
i18n_instance: JsonI18n = request.app['i18n']
|
||||
settings: Settings = request.app['settings']
|
||||
panel_service: PanelApiService = request.app['panel_service']
|
||||
subscription_service: SubscriptionService = request.app[
|
||||
'subscription_service']
|
||||
referral_service: ReferralService = request.app['referral_service']
|
||||
lknpd_service: Optional[LknpdService] = request.app.get('lknpd_service')
|
||||
async_session_factory: sessionmaker = request.app[
|
||||
'async_session_factory']
|
||||
bot: Bot = request.app["bot"]
|
||||
i18n_instance: JsonI18n = request.app["i18n"]
|
||||
settings: Settings = request.app["settings"]
|
||||
panel_service: PanelApiService = request.app["panel_service"]
|
||||
subscription_service: SubscriptionService = request.app["subscription_service"]
|
||||
referral_service: ReferralService = request.app["referral_service"]
|
||||
lknpd_service: Optional[LknpdService] = request.app.get("lknpd_service")
|
||||
async_session_factory: sessionmaker = request.app["async_session_factory"]
|
||||
except KeyError:
|
||||
logging.exception(
|
||||
"KeyError accessing app context in yookassa_webhook_route.")
|
||||
return web.Response(
|
||||
status=500,
|
||||
text="Internal Server Error: Missing app context component")
|
||||
logging.exception("KeyError accessing app context in yookassa_webhook_route.")
|
||||
return web.Response(status=500, text="Internal Server Error: Missing app context component")
|
||||
|
||||
client_ip = request_client_ip(request, trusted_proxies=settings.trusted_proxies)
|
||||
if not ip_in_allowlist(client_ip, YOOKASSA_WEBHOOK_ALLOWED_IPS):
|
||||
@@ -519,39 +550,41 @@ async def yookassa_webhook_route(request: web.Request):
|
||||
f"PaymentId='{payment_data_from_notification.id}', Status='{payment_data_from_notification.status}'"
|
||||
)
|
||||
|
||||
if not payment_data_from_notification or not hasattr(
|
||||
payment_data_from_notification,
|
||||
'metadata') or payment_data_from_notification.metadata is None:
|
||||
if (
|
||||
not payment_data_from_notification
|
||||
or not hasattr(payment_data_from_notification, "metadata")
|
||||
or payment_data_from_notification.metadata is None
|
||||
):
|
||||
logging.error(
|
||||
f"YooKassa webhook payment {payment_data_from_notification.id} lacks metadata. Cannot process."
|
||||
)
|
||||
return web.Response(status=200, text="ok_error_no_metadata")
|
||||
|
||||
# Safely extract payment_method details (SDK objects may not have to_dict)
|
||||
pm_obj = getattr(payment_data_from_notification, 'payment_method', None)
|
||||
pm_obj = getattr(payment_data_from_notification, "payment_method", None)
|
||||
pm_dict = None
|
||||
if pm_obj is not None:
|
||||
try:
|
||||
card_obj = getattr(pm_obj, 'card', None)
|
||||
card_obj = getattr(pm_obj, "card", None)
|
||||
pm_dict = {
|
||||
"id": getattr(pm_obj, 'id', None),
|
||||
"type": getattr(pm_obj, 'type', None),
|
||||
"saved": bool(getattr(pm_obj, 'saved', False)),
|
||||
"title": getattr(pm_obj, 'title', None),
|
||||
"id": getattr(pm_obj, "id", None),
|
||||
"type": getattr(pm_obj, "type", None),
|
||||
"saved": bool(getattr(pm_obj, "saved", False)),
|
||||
"title": getattr(pm_obj, "title", None),
|
||||
"account_number": (
|
||||
getattr(pm_obj, 'account_number', None)
|
||||
if hasattr(pm_obj, 'account_number') else (
|
||||
getattr(pm_obj, 'account', None)
|
||||
if hasattr(pm_obj, 'account') else None
|
||||
getattr(pm_obj, "account_number", None)
|
||||
if hasattr(pm_obj, "account_number")
|
||||
else (
|
||||
getattr(pm_obj, "account", None) if hasattr(pm_obj, "account") else None
|
||||
)
|
||||
),
|
||||
"card": (
|
||||
{
|
||||
"first6": getattr(card_obj, 'first6', None),
|
||||
"last4": getattr(card_obj, 'last4', None),
|
||||
"expiry_month": getattr(card_obj, 'expiry_month', None),
|
||||
"expiry_year": getattr(card_obj, 'expiry_year', None),
|
||||
"card_type": getattr(card_obj, 'card_type', None),
|
||||
"first6": getattr(card_obj, "first6", None),
|
||||
"last4": getattr(card_obj, "last4", None),
|
||||
"expiry_month": getattr(card_obj, "expiry_month", None),
|
||||
"expiry_year": getattr(card_obj, "expiry_year", None),
|
||||
"card_type": getattr(card_obj, "card_type", None),
|
||||
}
|
||||
if card_obj is not None
|
||||
else None
|
||||
@@ -562,21 +595,19 @@ async def yookassa_webhook_route(request: web.Request):
|
||||
pm_dict = None
|
||||
|
||||
payment_dict_for_processing = {
|
||||
"id":
|
||||
str(payment_data_from_notification.id),
|
||||
"status":
|
||||
str(payment_data_from_notification.status),
|
||||
"paid":
|
||||
bool(payment_data_from_notification.paid),
|
||||
"id": str(payment_data_from_notification.id),
|
||||
"status": str(payment_data_from_notification.status),
|
||||
"paid": bool(payment_data_from_notification.paid),
|
||||
"amount": {
|
||||
"value": str(payment_data_from_notification.amount.value),
|
||||
"currency": str(payment_data_from_notification.amount.currency)
|
||||
} if payment_data_from_notification.amount else {},
|
||||
"metadata":
|
||||
dict(payment_data_from_notification.metadata),
|
||||
"description":
|
||||
str(payment_data_from_notification.description)
|
||||
if payment_data_from_notification.description else None,
|
||||
"currency": str(payment_data_from_notification.amount.currency),
|
||||
}
|
||||
if payment_data_from_notification.amount
|
||||
else {},
|
||||
"metadata": dict(payment_data_from_notification.metadata),
|
||||
"description": str(payment_data_from_notification.description)
|
||||
if payment_data_from_notification.description
|
||||
else None,
|
||||
"payment_method": pm_dict,
|
||||
}
|
||||
|
||||
@@ -584,14 +615,21 @@ async def yookassa_webhook_route(request: web.Request):
|
||||
async with async_session_factory() as session:
|
||||
try:
|
||||
if notification_object.event == YOOKASSA_EVENT_PAYMENT_SUCCEEDED:
|
||||
if payment_dict_for_processing.get(
|
||||
"paid") and payment_dict_for_processing.get(
|
||||
"status") == "succeeded":
|
||||
if (
|
||||
payment_dict_for_processing.get("paid")
|
||||
and payment_dict_for_processing.get("status") == "succeeded"
|
||||
):
|
||||
await process_successful_payment(
|
||||
session, bot, payment_dict_for_processing,
|
||||
i18n_instance, settings, panel_service,
|
||||
subscription_service, referral_service,
|
||||
lknpd_service)
|
||||
session,
|
||||
bot,
|
||||
payment_dict_for_processing,
|
||||
i18n_instance,
|
||||
settings,
|
||||
panel_service,
|
||||
subscription_service,
|
||||
referral_service,
|
||||
lknpd_service,
|
||||
)
|
||||
await session.commit()
|
||||
else:
|
||||
logging.warning(
|
||||
@@ -601,37 +639,62 @@ async def yookassa_webhook_route(request: web.Request):
|
||||
)
|
||||
elif notification_object.event == YOOKASSA_EVENT_PAYMENT_CANCELED:
|
||||
await process_cancelled_payment(
|
||||
session, bot, payment_dict_for_processing,
|
||||
i18n_instance, settings)
|
||||
session, bot, payment_dict_for_processing, i18n_instance, settings
|
||||
)
|
||||
await session.commit()
|
||||
elif notification_object.event == YOOKASSA_EVENT_PAYMENT_WAITING_FOR_CAPTURE:
|
||||
# Bind-only flow: save method and cancel auth if metadata has bind_only
|
||||
metadata = payment_dict_for_processing.get("metadata", {}) or {}
|
||||
if settings.yookassa_autopayments_active and metadata.get("bind_only") == "1":
|
||||
if (
|
||||
settings.yookassa_autopayments_active
|
||||
and metadata.get("bind_only") == "1"
|
||||
):
|
||||
try:
|
||||
user_id_str = metadata.get("user_id")
|
||||
if user_id_str and user_id_str.isdigit():
|
||||
user_id = int(user_id_str)
|
||||
payment_method = payment_dict_for_processing.get("payment_method")
|
||||
if isinstance(payment_method, dict) and payment_method.get("id"):
|
||||
payment_method = payment_dict_for_processing.get(
|
||||
"payment_method"
|
||||
)
|
||||
if isinstance(payment_method, dict) and payment_method.get(
|
||||
"id"
|
||||
):
|
||||
pm_type = payment_method.get("type")
|
||||
title = payment_method.get("title")
|
||||
card = payment_method.get("card") or {}
|
||||
account_number = payment_method.get("account_number") or payment_method.get("account")
|
||||
account_number = payment_method.get(
|
||||
"account_number"
|
||||
) or payment_method.get("account")
|
||||
display_network = None
|
||||
display_last4 = None
|
||||
if (pm_type or "").lower() in {"bank_card", "bank-card", "card"}:
|
||||
display_network = card.get("card_type") or title or "Card"
|
||||
if (pm_type or "").lower() in {
|
||||
"bank_card",
|
||||
"bank-card",
|
||||
"card",
|
||||
}:
|
||||
display_network = (
|
||||
card.get("card_type") or title or "Card"
|
||||
)
|
||||
display_last4 = card.get("last4")
|
||||
elif (pm_type or "").lower() in {"yoo_money", "yoomoney", "yoo-money", "wallet"}:
|
||||
elif (pm_type or "").lower() in {
|
||||
"yoo_money",
|
||||
"yoomoney",
|
||||
"yoo-money",
|
||||
"wallet",
|
||||
}:
|
||||
# Normalize wallet display name to avoid leaking full account from title
|
||||
display_network = "YooMoney"
|
||||
if isinstance(account_number, str) and len(account_number) >= 4:
|
||||
if (
|
||||
isinstance(account_number, str)
|
||||
and len(account_number) >= 4
|
||||
):
|
||||
display_last4 = account_number[-4:]
|
||||
else:
|
||||
display_last4 = None
|
||||
else:
|
||||
display_network = title or (pm_type.upper() if pm_type else "Payment method")
|
||||
display_network = title or (
|
||||
pm_type.upper() if pm_type else "Payment method"
|
||||
)
|
||||
display_last4 = None
|
||||
await user_billing_dal.upsert_yk_payment_method(
|
||||
session,
|
||||
@@ -644,6 +707,7 @@ async def yookassa_webhook_route(request: web.Request):
|
||||
# Save multi-card entry and mark default if first
|
||||
try:
|
||||
from db.dal import user_billing_dal as ub
|
||||
|
||||
await ub.upsert_user_payment_method(
|
||||
session,
|
||||
user_id=user_id,
|
||||
@@ -661,35 +725,53 @@ async def yookassa_webhook_route(request: web.Request):
|
||||
# Use user's DB language for bind success notification
|
||||
i18n_lang = settings.DEFAULT_LANGUAGE
|
||||
from db.dal import user_dal
|
||||
db_user = await user_dal.get_user_by_id(session, user_id)
|
||||
|
||||
db_user = await user_dal.get_user_by_id(
|
||||
session, user_id
|
||||
)
|
||||
if db_user and db_user.language_code:
|
||||
i18n_lang = db_user.language_code
|
||||
_ = lambda key, **kwargs: i18n_instance.gettext(i18n_lang, key, **kwargs)
|
||||
from bot.keyboards.inline.user_keyboards import get_back_to_payment_methods_keyboard
|
||||
_ = lambda key, **kwargs: i18n_instance.gettext(
|
||||
i18n_lang, key, **kwargs
|
||||
)
|
||||
from bot.keyboards.inline.user_keyboards import (
|
||||
get_back_to_payment_methods_keyboard,
|
||||
)
|
||||
|
||||
await bot.send_message(
|
||||
chat_id=user_id,
|
||||
text=_("payment_method_bound_success"),
|
||||
reply_markup=get_back_to_payment_methods_keyboard(i18n_lang, i18n_instance)
|
||||
reply_markup=get_back_to_payment_methods_keyboard(
|
||||
i18n_lang, i18n_instance
|
||||
),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
# Attempt to cancel the authorization to avoid charge hold
|
||||
try:
|
||||
yk: YooKassaService = request.app.get('yookassa_service')
|
||||
yk: YooKassaService = request.app.get(
|
||||
"yookassa_service"
|
||||
)
|
||||
if yk:
|
||||
await yk.cancel_payment(payment_dict_for_processing.get("id"))
|
||||
await yk.cancel_payment(
|
||||
payment_dict_for_processing.get("id")
|
||||
)
|
||||
except Exception:
|
||||
logging.exception("Failed to cancel bind-only payment auth")
|
||||
logging.exception(
|
||||
"Failed to cancel bind-only payment auth"
|
||||
)
|
||||
except Exception:
|
||||
logging.exception("Failed to handle bind-only waiting_for_capture webhook")
|
||||
logging.exception(
|
||||
"Failed to handle bind-only waiting_for_capture webhook"
|
||||
)
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
logging.exception(
|
||||
"Error processing YooKassa webhook event '%s' for YK Payment ID %s in DB transaction.",
|
||||
notification_object.event,
|
||||
payment_dict_for_processing.get('id'))
|
||||
return web.Response(
|
||||
status=500, text="internal_processing_error")
|
||||
payment_dict_for_processing.get("id"),
|
||||
)
|
||||
return web.Response(status=500, text="internal_processing_error")
|
||||
|
||||
return web.Response(status=200, text="ok")
|
||||
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import logging
|
||||
import re
|
||||
from aiogram import Router, F, types, Bot
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from aiogram import Bot, F, Router, types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from config.settings import Settings
|
||||
from bot.states.user_states import UserPromoStates
|
||||
from bot.services.promo_code_service import PromoCodeService
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
from bot.keyboards.inline.user_keyboards import (
|
||||
get_back_to_main_menu_markup,
|
||||
get_connect_and_main_keyboard,
|
||||
)
|
||||
from datetime import datetime
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
from bot.services.promo_code_service import PromoCodeService
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
from bot.states.user_states import UserPromoStates
|
||||
from bot.utils.callback_answer import safe_answer_callback
|
||||
from config.settings import Settings
|
||||
|
||||
from .start import send_main_menu
|
||||
|
||||
@@ -24,15 +25,20 @@ router = Router(name="user_promo_router")
|
||||
SUSPICIOUS_SQL_KEYWORDS_REGEX = re.compile(
|
||||
r"\b(DROP\s*TABLE|DELETE\s*FROM|ALTER\s*TABLE|TRUNCATE\s*TABLE|UNION\s*SELECT|"
|
||||
r";\s*SELECT|;\s*INSERT|;\s*UPDATE|;\s*DELETE|xp_cmdshell|sysdatabases|sysobjects|INFORMATION_SCHEMA)\b",
|
||||
re.IGNORECASE)
|
||||
re.IGNORECASE,
|
||||
)
|
||||
SUSPICIOUS_CHARS_REGEX = re.compile(r"(--|#\s|;|\*\/|\/\*)")
|
||||
MAX_PROMO_CODE_INPUT_LENGTH = 100
|
||||
|
||||
|
||||
async def prompt_promo_code_input(callback: types.CallbackQuery,
|
||||
state: FSMContext, i18n_data: dict,
|
||||
settings: Settings, session: AsyncSession,
|
||||
back_callback: str = "main_action:back_to_main"):
|
||||
async def prompt_promo_code_input(
|
||||
callback: types.CallbackQuery,
|
||||
state: FSMContext,
|
||||
i18n_data: dict,
|
||||
settings: Settings,
|
||||
session: AsyncSession,
|
||||
back_callback: str = "main_action:back_to_main",
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
@@ -41,8 +47,7 @@ async def prompt_promo_code_input(callback: types.CallbackQuery,
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
|
||||
if not callback.message:
|
||||
logging.error(
|
||||
"CallbackQuery has no message in prompt_promo_code_input")
|
||||
logging.error("CallbackQuery has no message in prompt_promo_code_input")
|
||||
await safe_answer_callback(
|
||||
callback,
|
||||
_("error_occurred_processing_request"),
|
||||
@@ -57,32 +62,38 @@ async def prompt_promo_code_input(callback: types.CallbackQuery,
|
||||
current_lang,
|
||||
i18n,
|
||||
callback_data=back_callback,
|
||||
))
|
||||
except Exception as e_edit:
|
||||
logging.warning(
|
||||
f"Failed to edit message for promo prompt: {e_edit}. Sending new one."
|
||||
),
|
||||
)
|
||||
except Exception as e_edit:
|
||||
logging.warning(f"Failed to edit message for promo prompt: {e_edit}. Sending new one.")
|
||||
await callback.message.answer(
|
||||
text=_(key="promo_code_prompt"),
|
||||
reply_markup=get_back_to_main_menu_markup(
|
||||
current_lang,
|
||||
i18n,
|
||||
callback_data=back_callback,
|
||||
))
|
||||
),
|
||||
)
|
||||
|
||||
await safe_answer_callback(callback)
|
||||
await state.set_state(UserPromoStates.waiting_for_promo_code)
|
||||
logging.info(
|
||||
f"User {callback.from_user.id} entered state UserPromoStates.waiting_for_promo_code. "
|
||||
f"FSM state: {await state.get_state()}")
|
||||
f"FSM state: {await state.get_state()}"
|
||||
)
|
||||
|
||||
|
||||
@router.message(UserPromoStates.waiting_for_promo_code, F.text)
|
||||
async def process_promo_code_input(message: types.Message, state: FSMContext,
|
||||
settings: Settings, i18n_data: dict,
|
||||
promo_code_service: PromoCodeService,
|
||||
subscription_service: SubscriptionService,
|
||||
bot: Bot, session: AsyncSession):
|
||||
async def process_promo_code_input(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
settings: Settings,
|
||||
i18n_data: dict,
|
||||
promo_code_service: PromoCodeService,
|
||||
subscription_service: SubscriptionService,
|
||||
bot: Bot,
|
||||
session: AsyncSession,
|
||||
):
|
||||
logging.info(
|
||||
f"Processing promo code input from user {message.from_user.id} in state {await state.get_state()}: '{message.text}'"
|
||||
)
|
||||
@@ -91,9 +102,7 @@ async def process_promo_code_input(message: types.Message, state: FSMContext,
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
|
||||
if not i18n or not promo_code_service:
|
||||
logging.error(
|
||||
"Dependencies (i18n or PromoCodeService) missing in process_promo_code_input"
|
||||
)
|
||||
logging.error("Dependencies (i18n or PromoCodeService) missing in process_promo_code_input")
|
||||
await message.reply("Service error. Please try again later.")
|
||||
await state.clear()
|
||||
return
|
||||
@@ -106,10 +115,11 @@ async def process_promo_code_input(message: types.Message, state: FSMContext,
|
||||
if not code_input:
|
||||
is_suspicious = True
|
||||
logging.warning(f"Empty promo code input by user {user.id}.")
|
||||
elif len(
|
||||
code_input
|
||||
) > MAX_PROMO_CODE_INPUT_LENGTH or SUSPICIOUS_SQL_KEYWORDS_REGEX.search(
|
||||
code_input) or SUSPICIOUS_CHARS_REGEX.search(code_input):
|
||||
elif (
|
||||
len(code_input) > MAX_PROMO_CODE_INPUT_LENGTH
|
||||
or SUSPICIOUS_SQL_KEYWORDS_REGEX.search(code_input)
|
||||
or SUSPICIOUS_CHARS_REGEX.search(code_input)
|
||||
):
|
||||
is_suspicious = True
|
||||
logging.warning(
|
||||
f"Suspicious input for promo code by user {user.id} (len: {len(code_input)}): '{code_input}'"
|
||||
@@ -121,23 +131,23 @@ async def process_promo_code_input(message: types.Message, state: FSMContext,
|
||||
if settings.LOG_SUSPICIOUS_ACTIVITY:
|
||||
try:
|
||||
from bot.services.notification_service import NotificationService
|
||||
|
||||
notification_service = NotificationService(bot, settings, i18n)
|
||||
await notification_service.notify_suspicious_promo_attempt(
|
||||
user_id=user.id,
|
||||
username=user.username,
|
||||
first_name=user.first_name,
|
||||
suspicious_input=code_input
|
||||
suspicious_input=code_input,
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to send suspicious promo notification: {e}")
|
||||
|
||||
success, result = await promo_code_service.apply_promo_code(
|
||||
session, user.id, code_input, current_lang)
|
||||
session, user.id, code_input, current_lang
|
||||
)
|
||||
if success:
|
||||
await session.commit()
|
||||
logging.info(
|
||||
f"Promo code '{code_input}' successfully applied for user {user.id}."
|
||||
)
|
||||
logging.info(f"Promo code '{code_input}' successfully applied for user {user.id}.")
|
||||
|
||||
new_end_date = result if isinstance(result, datetime) else None
|
||||
active = await subscription_service.get_active_subscription_details(session, user.id)
|
||||
@@ -163,9 +173,7 @@ async def process_promo_code_input(message: types.Message, state: FSMContext,
|
||||
f"Promo code '{code_input}' application failed for user {user.id}. Reason: {result}"
|
||||
)
|
||||
response_to_user_text = result
|
||||
reply_markup = get_back_to_main_menu_markup(
|
||||
current_lang, i18n
|
||||
)
|
||||
reply_markup = get_back_to_main_menu_markup(current_lang, i18n)
|
||||
|
||||
await message.answer(
|
||||
response_to_user_text,
|
||||
@@ -178,12 +186,15 @@ async def process_promo_code_input(message: types.Message, state: FSMContext,
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "main_action:back_to_main",
|
||||
UserPromoStates.waiting_for_promo_code)
|
||||
@router.callback_query(F.data == "main_action:back_to_main", UserPromoStates.waiting_for_promo_code)
|
||||
async def cancel_promo_input_via_button(
|
||||
callback: types.CallbackQuery, state: FSMContext, settings: Settings,
|
||||
i18n_data: dict, subscription_service: SubscriptionService,
|
||||
session: AsyncSession):
|
||||
callback: types.CallbackQuery,
|
||||
state: FSMContext,
|
||||
settings: Settings,
|
||||
i18n_data: dict,
|
||||
subscription_service: SubscriptionService,
|
||||
session: AsyncSession,
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
@@ -197,15 +208,10 @@ async def cancel_promo_input_via_button(
|
||||
await state.clear()
|
||||
|
||||
if callback.message:
|
||||
|
||||
await send_main_menu(callback,
|
||||
settings,
|
||||
i18n_data,
|
||||
subscription_service,
|
||||
session,
|
||||
is_edit=True)
|
||||
await send_main_menu(
|
||||
callback, settings, i18n_data, subscription_service, session, is_edit=True
|
||||
)
|
||||
else:
|
||||
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
await safe_answer_callback(
|
||||
callback,
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
import logging
|
||||
from aiogram import Router, F, types, Bot
|
||||
from aiogram.filters import Command
|
||||
from typing import Optional, Union
|
||||
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
|
||||
|
||||
from aiogram import Bot, F, Router, types
|
||||
from aiogram.filters import Command
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
from bot.services.referral_service import ReferralService
|
||||
from config.settings import Settings
|
||||
from db.dal import user_dal
|
||||
from bot.services.referral_service import ReferralService
|
||||
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
|
||||
router = Router(name="user_referral_router")
|
||||
|
||||
|
||||
async def referral_command_handler(event: Union[types.Message, types.CallbackQuery],
|
||||
settings: Settings, i18n_data: dict,
|
||||
referral_service: ReferralService, bot: Bot,
|
||||
session: AsyncSession,
|
||||
back_callback: str = "main_action:back_to_main"):
|
||||
async def referral_command_handler(
|
||||
event: Union[types.Message, types.CallbackQuery],
|
||||
settings: Settings,
|
||||
i18n_data: dict,
|
||||
referral_service: ReferralService,
|
||||
bot: Bot,
|
||||
session: AsyncSession,
|
||||
back_callback: str = "main_action:back_to_main",
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
|
||||
target_message_obj = event.message if isinstance(
|
||||
event, types.CallbackQuery) else event
|
||||
target_message_obj = event.message if isinstance(event, types.CallbackQuery) else event
|
||||
if not target_message_obj:
|
||||
logging.error(
|
||||
"Target message is None in referral_command_handler (possibly from callback without message)."
|
||||
)
|
||||
if isinstance(event, types.CallbackQuery):
|
||||
await event.answer("Error displaying referral info.",
|
||||
show_alert=True)
|
||||
await event.answer("Error displaying referral info.", show_alert=True)
|
||||
return
|
||||
|
||||
if not i18n or not referral_service:
|
||||
logging.error(
|
||||
"Dependencies (i18n or ReferralService) missing in referral_command_handler"
|
||||
)
|
||||
await target_message_obj.answer(
|
||||
"Service error. Please try again later.")
|
||||
if isinstance(event, types.CallbackQuery): await event.answer()
|
||||
logging.error("Dependencies (i18n or ReferralService) missing in referral_command_handler")
|
||||
await target_message_obj.answer("Service error. Please try again later.")
|
||||
if isinstance(event, types.CallbackQuery):
|
||||
await event.answer()
|
||||
return
|
||||
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
@@ -48,21 +48,23 @@ async def referral_command_handler(event: Union[types.Message, types.CallbackQue
|
||||
bot_info = await bot.get_me()
|
||||
bot_username = bot_info.username
|
||||
except Exception as e_bot_info:
|
||||
logging.error(
|
||||
f"Failed to get bot info for referral link: {e_bot_info}")
|
||||
logging.error(f"Failed to get bot info for referral link: {e_bot_info}")
|
||||
await target_message_obj.answer(_("error_generating_referral_link"))
|
||||
if isinstance(event, types.CallbackQuery): await event.answer()
|
||||
if isinstance(event, types.CallbackQuery):
|
||||
await event.answer()
|
||||
return
|
||||
|
||||
if not bot_username:
|
||||
logging.error("Bot username is None, cannot generate referral link.")
|
||||
await target_message_obj.answer(_("error_generating_referral_link"))
|
||||
if isinstance(event, types.CallbackQuery): await event.answer()
|
||||
if isinstance(event, types.CallbackQuery):
|
||||
await event.answer()
|
||||
return
|
||||
|
||||
inviter_user_id = event.from_user.id
|
||||
referral_link = await referral_service.generate_referral_link(
|
||||
session, bot_username, inviter_user_id)
|
||||
session, bot_username, inviter_user_id
|
||||
)
|
||||
|
||||
if not referral_link:
|
||||
logging.error(
|
||||
@@ -79,22 +81,26 @@ async def referral_command_handler(event: Union[types.Message, types.CallbackQue
|
||||
bonus_details_str = _("referral_not_available_for_traffic")
|
||||
else:
|
||||
if settings.subscription_options:
|
||||
for months_period_key, _price in sorted(
|
||||
settings.subscription_options.items()):
|
||||
|
||||
for months_period_key, _price in sorted(settings.subscription_options.items()):
|
||||
inv_bonus = settings.referral_bonus_inviter.get(months_period_key)
|
||||
ref_bonus = settings.referral_bonus_referee.get(months_period_key)
|
||||
if inv_bonus is not None or ref_bonus is not None:
|
||||
bonus_info_parts.append(
|
||||
_("referral_bonus_per_period",
|
||||
months=months_period_key,
|
||||
inviter_bonus_days=inv_bonus
|
||||
if inv_bonus is not None else _("no_bonus_placeholder"),
|
||||
referee_bonus_days=ref_bonus
|
||||
if ref_bonus is not None else _("no_bonus_placeholder")))
|
||||
_(
|
||||
"referral_bonus_per_period",
|
||||
months=months_period_key,
|
||||
inviter_bonus_days=inv_bonus
|
||||
if inv_bonus is not None
|
||||
else _("no_bonus_placeholder"),
|
||||
referee_bonus_days=ref_bonus
|
||||
if ref_bonus is not None
|
||||
else _("no_bonus_placeholder"),
|
||||
)
|
||||
)
|
||||
|
||||
bonus_details_str = "\n".join(bonus_info_parts) if bonus_info_parts else _(
|
||||
"referral_no_bonuses_configured")
|
||||
bonus_details_str = (
|
||||
"\n".join(bonus_info_parts) if bonus_info_parts else _("referral_no_bonuses_configured")
|
||||
)
|
||||
|
||||
referral_stats = await referral_service.get_referral_stats(session, inviter_user_id)
|
||||
|
||||
@@ -112,14 +118,17 @@ async def referral_command_handler(event: Union[types.Message, types.CallbackQue
|
||||
else ""
|
||||
)
|
||||
|
||||
text = _("referral_program_info_new",
|
||||
referral_link=referral_link,
|
||||
webapp_link_section=webapp_link_section,
|
||||
bonus_details=bonus_details_str,
|
||||
invited_count=referral_stats["invited_count"],
|
||||
purchased_count=referral_stats["purchased_count"])
|
||||
text = _(
|
||||
"referral_program_info_new",
|
||||
referral_link=referral_link,
|
||||
webapp_link_section=webapp_link_section,
|
||||
bonus_details=bonus_details_str,
|
||||
invited_count=referral_stats["invited_count"],
|
||||
purchased_count=referral_stats["purchased_count"],
|
||||
)
|
||||
|
||||
from bot.keyboards.inline.user_keyboards import get_referral_link_keyboard
|
||||
|
||||
reply_markup_val = get_referral_link_keyboard(
|
||||
current_lang,
|
||||
i18n,
|
||||
@@ -127,28 +136,29 @@ async def referral_command_handler(event: Union[types.Message, types.CallbackQue
|
||||
)
|
||||
|
||||
if isinstance(event, types.Message):
|
||||
await event.answer(text,
|
||||
reply_markup=reply_markup_val,
|
||||
disable_web_page_preview=True)
|
||||
await event.answer(text, reply_markup=reply_markup_val, disable_web_page_preview=True)
|
||||
elif isinstance(event, types.CallbackQuery) and event.message:
|
||||
try:
|
||||
await event.message.edit_text(text,
|
||||
reply_markup=reply_markup_val,
|
||||
disable_web_page_preview=True)
|
||||
await event.message.edit_text(
|
||||
text, reply_markup=reply_markup_val, disable_web_page_preview=True
|
||||
)
|
||||
except Exception as e_edit:
|
||||
logging.warning(
|
||||
f"Failed to edit message for referral info: {e_edit}. Sending new one."
|
||||
logging.warning(f"Failed to edit message for referral info: {e_edit}. Sending new one.")
|
||||
await event.message.answer(
|
||||
text, reply_markup=reply_markup_val, disable_web_page_preview=True
|
||||
)
|
||||
await event.message.answer(text,
|
||||
reply_markup=reply_markup_val,
|
||||
disable_web_page_preview=True)
|
||||
await event.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("referral_action:"))
|
||||
async def referral_action_handler(callback: types.CallbackQuery, settings: Settings,
|
||||
i18n_data: dict, referral_service: ReferralService,
|
||||
bot: Bot, session: AsyncSession):
|
||||
async def referral_action_handler(
|
||||
callback: types.CallbackQuery,
|
||||
settings: Settings,
|
||||
i18n_data: dict,
|
||||
referral_service: ReferralService,
|
||||
bot: Bot,
|
||||
session: AsyncSession,
|
||||
):
|
||||
action = callback.data.split(":")[1]
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n = i18n_data.get("i18n_instance")
|
||||
@@ -164,7 +174,8 @@ async def referral_action_handler(callback: types.CallbackQuery, settings: Setti
|
||||
|
||||
inviter_user_id = callback.from_user.id
|
||||
referral_link = await referral_service.generate_referral_link(
|
||||
session, bot_username, inviter_user_id)
|
||||
session, bot_username, inviter_user_id
|
||||
)
|
||||
|
||||
if not referral_link:
|
||||
logging.error(
|
||||
@@ -188,10 +199,7 @@ async def referral_action_handler(callback: types.CallbackQuery, settings: Setti
|
||||
else:
|
||||
friend_message = _("referral_friend_message", referral_link=referral_link)
|
||||
|
||||
await callback.message.answer(
|
||||
friend_message,
|
||||
disable_web_page_preview=True
|
||||
)
|
||||
await callback.message.answer(friend_message, disable_web_page_preview=True)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error in referral share message: {e}")
|
||||
@@ -200,7 +208,9 @@ async def referral_action_handler(callback: types.CallbackQuery, settings: Setti
|
||||
await callback.answer()
|
||||
|
||||
|
||||
def _build_webapp_referral_link(base_url: Optional[str], referral_code: Optional[str]) -> Optional[str]:
|
||||
def _build_webapp_referral_link(
|
||||
base_url: Optional[str], referral_code: Optional[str]
|
||||
) -> Optional[str]:
|
||||
if not base_url or not referral_code:
|
||||
return None
|
||||
parts = urlsplit(base_url)
|
||||
@@ -233,7 +243,12 @@ async def _generate_webapp_referral_link(
|
||||
|
||||
|
||||
@router.message(Command("referral"))
|
||||
async def referral_command_message_handler(message: types.Message, settings: Settings,
|
||||
i18n_data: dict, referral_service: ReferralService,
|
||||
bot: Bot, session: AsyncSession):
|
||||
async def referral_command_message_handler(
|
||||
message: types.Message,
|
||||
settings: Settings,
|
||||
i18n_data: dict,
|
||||
referral_service: ReferralService,
|
||||
bot: Bot,
|
||||
session: AsyncSession,
|
||||
):
|
||||
await referral_command_handler(message, settings, i18n_data, referral_service, bot, session)
|
||||
|
||||
+239
-245
@@ -1,61 +1,62 @@
|
||||
import logging
|
||||
import re
|
||||
from aiogram import Router, F, types, Bot
|
||||
from aiogram.utils.text_decorations import html_decoration as hd
|
||||
from aiogram.filters import CommandStart, Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from typing import Optional, Union
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from datetime import datetime, timezone
|
||||
from aiogram.exceptions import TelegramAPIError, TelegramBadRequest, TelegramForbiddenError
|
||||
from typing import Optional, Union
|
||||
|
||||
from aiogram import Bot, F, Router, types
|
||||
from aiogram.exceptions import TelegramAPIError, TelegramBadRequest, TelegramForbiddenError
|
||||
from aiogram.filters import Command, CommandStart
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.utils.text_decorations import html_decoration as hd
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from bot.keyboards.inline.user_keyboards import (
|
||||
get_bot_interface_inline_keyboard,
|
||||
get_channel_subscription_keyboard,
|
||||
get_information_links_keyboard,
|
||||
get_language_selection_keyboard,
|
||||
get_main_menu_inline_keyboard,
|
||||
)
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
from bot.services.panel_api_service import PanelApiService
|
||||
from bot.services.promo_code_service import PromoCodeService
|
||||
from bot.services.referral_service import ReferralService
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
from bot.utils.callback_answer import safe_answer_callback
|
||||
from bot.utils.text_sanitizer import sanitize_display_name, sanitize_username
|
||||
from config.settings import Settings
|
||||
from db.dal import user_dal
|
||||
from db.models import User
|
||||
|
||||
from bot.keyboards.inline.user_keyboards import (
|
||||
get_main_menu_inline_keyboard,
|
||||
get_bot_interface_inline_keyboard,
|
||||
get_language_selection_keyboard,
|
||||
get_channel_subscription_keyboard,
|
||||
get_information_links_keyboard,
|
||||
)
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
from bot.services.panel_api_service import PanelApiService
|
||||
from bot.services.referral_service import ReferralService
|
||||
from bot.services.promo_code_service import PromoCodeService
|
||||
from config.settings import Settings
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
from bot.utils.text_sanitizer import sanitize_username, sanitize_display_name
|
||||
from bot.utils.callback_answer import safe_answer_callback
|
||||
router = Router(name="user_start_router")
|
||||
|
||||
|
||||
async def should_show_trial_button(
|
||||
settings: Settings,
|
||||
subscription_service: SubscriptionService,
|
||||
session: AsyncSession,
|
||||
user_id: int) -> bool:
|
||||
settings: Settings,
|
||||
subscription_service: SubscriptionService,
|
||||
session: AsyncSession,
|
||||
user_id: int,
|
||||
) -> bool:
|
||||
if not settings.TRIAL_ENABLED:
|
||||
return False
|
||||
|
||||
if hasattr(subscription_service, 'has_had_any_subscription') and callable(
|
||||
getattr(subscription_service, 'has_had_any_subscription')):
|
||||
return not await subscription_service.has_had_any_subscription(
|
||||
session, user_id)
|
||||
if hasattr(subscription_service, "has_had_any_subscription") and callable(
|
||||
getattr(subscription_service, "has_had_any_subscription")
|
||||
):
|
||||
return not await subscription_service.has_had_any_subscription(session, user_id)
|
||||
|
||||
logging.error(
|
||||
"Method has_had_any_subscription is missing in SubscriptionService!"
|
||||
)
|
||||
logging.error("Method has_had_any_subscription is missing in SubscriptionService!")
|
||||
return False
|
||||
|
||||
|
||||
async def send_main_menu(target_event: Union[types.Message,
|
||||
types.CallbackQuery],
|
||||
settings: Settings,
|
||||
i18n_data: dict,
|
||||
subscription_service: SubscriptionService,
|
||||
session: AsyncSession,
|
||||
is_edit: bool = False):
|
||||
async def send_main_menu(
|
||||
target_event: Union[types.Message, types.CallbackQuery],
|
||||
settings: Settings,
|
||||
i18n_data: dict,
|
||||
subscription_service: SubscriptionService,
|
||||
session: AsyncSession,
|
||||
is_edit: bool = False,
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
|
||||
@@ -63,8 +64,7 @@ async def send_main_menu(target_event: Union[types.Message,
|
||||
user_full_name = hd.quote(target_event.from_user.full_name)
|
||||
|
||||
if not i18n:
|
||||
logging.error(
|
||||
f"i18n_instance missing in send_main_menu for user {user_id}")
|
||||
logging.error(f"i18n_instance missing in send_main_menu for user {user_id}")
|
||||
err_msg_fallback = "Error: Language service unavailable. Please try again later."
|
||||
if isinstance(target_event, types.CallbackQuery):
|
||||
try:
|
||||
@@ -78,27 +78,25 @@ async def send_main_menu(target_event: Union[types.Message,
|
||||
pass
|
||||
return
|
||||
|
||||
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||
|
||||
show_trial_button_in_menu = await should_show_trial_button(
|
||||
settings, subscription_service, session, user_id)
|
||||
settings, subscription_service, session, user_id
|
||||
)
|
||||
|
||||
text = _(key="main_menu_greeting", user_name=user_full_name)
|
||||
reply_markup = get_main_menu_inline_keyboard(current_lang, i18n, settings,
|
||||
show_trial_button_in_menu)
|
||||
reply_markup = get_main_menu_inline_keyboard(
|
||||
current_lang, i18n, settings, show_trial_button_in_menu
|
||||
)
|
||||
|
||||
target_message_obj: Optional[types.Message] = None
|
||||
if isinstance(target_event, types.Message):
|
||||
target_message_obj = target_event
|
||||
elif isinstance(target_event,
|
||||
types.CallbackQuery) and target_event.message:
|
||||
elif isinstance(target_event, types.CallbackQuery) and target_event.message:
|
||||
target_message_obj = target_event.message
|
||||
|
||||
if not target_message_obj:
|
||||
logging.error(
|
||||
f"send_main_menu: target_message_obj is None for event from user {user_id}."
|
||||
)
|
||||
logging.error(f"send_main_menu: target_message_obj is None for event from user {user_id}.")
|
||||
if isinstance(target_event, types.CallbackQuery):
|
||||
await safe_answer_callback(
|
||||
target_event,
|
||||
@@ -134,12 +132,13 @@ async def send_main_menu(target_event: Union[types.Message,
|
||||
|
||||
|
||||
async def send_bot_interface_menu(
|
||||
target_event: Union[types.Message, types.CallbackQuery],
|
||||
settings: Settings,
|
||||
i18n_data: dict,
|
||||
subscription_service: SubscriptionService,
|
||||
session: AsyncSession,
|
||||
is_edit: bool = False):
|
||||
target_event: Union[types.Message, types.CallbackQuery],
|
||||
settings: Settings,
|
||||
i18n_data: dict,
|
||||
subscription_service: SubscriptionService,
|
||||
session: AsyncSession,
|
||||
is_edit: bool = False,
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
|
||||
@@ -149,16 +148,15 @@ async def send_bot_interface_menu(
|
||||
|
||||
user_id = target_event.from_user.id
|
||||
show_trial_button_in_menu = await should_show_trial_button(
|
||||
settings, subscription_service, session, user_id)
|
||||
settings, subscription_service, session, user_id
|
||||
)
|
||||
|
||||
text = i18n.gettext(current_lang, "bot_interface_menu_title")
|
||||
if settings.SUBSCRIPTION_MINI_APP_URL:
|
||||
text = (
|
||||
f"{text}\n\n"
|
||||
f"{i18n.gettext(current_lang, 'bot_interface_menu_webapp_hint')}"
|
||||
)
|
||||
text = f"{text}\n\n{i18n.gettext(current_lang, 'bot_interface_menu_webapp_hint')}"
|
||||
reply_markup = get_bot_interface_inline_keyboard(
|
||||
current_lang, i18n, settings, show_trial_button_in_menu)
|
||||
current_lang, i18n, settings, show_trial_button_in_menu
|
||||
)
|
||||
|
||||
target_message_obj: Optional[types.Message] = None
|
||||
if isinstance(target_event, types.Message):
|
||||
@@ -201,12 +199,13 @@ async def send_bot_interface_menu(
|
||||
|
||||
|
||||
async def ensure_required_channel_subscription(
|
||||
event: Union[types.Message, types.CallbackQuery],
|
||||
settings: Settings,
|
||||
i18n: Optional[JsonI18n],
|
||||
current_lang: str,
|
||||
session: AsyncSession,
|
||||
db_user: Optional[User] = None) -> bool:
|
||||
event: Union[types.Message, types.CallbackQuery],
|
||||
settings: Settings,
|
||||
i18n: Optional[JsonI18n],
|
||||
current_lang: str,
|
||||
session: AsyncSession,
|
||||
db_user: Optional[User] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Verify that the user is a member of the required channel (if configured).
|
||||
Returns True when access can proceed, False when user must subscribe first.
|
||||
@@ -227,9 +226,7 @@ async def ensure_required_channel_subscription(
|
||||
message_obj = event
|
||||
|
||||
if bot_instance is None:
|
||||
logging.error(
|
||||
"Channel subscription check: bot instance missing for user %s.", user_id
|
||||
)
|
||||
logging.error("Channel subscription check: bot instance missing for user %s.", user_id)
|
||||
return False
|
||||
|
||||
if user_id in settings.ADMIN_IDS:
|
||||
@@ -254,9 +251,10 @@ async def ensure_required_channel_subscription(
|
||||
)
|
||||
return True
|
||||
|
||||
if (db_user.channel_subscription_verified
|
||||
and db_user.channel_subscription_verified_for
|
||||
== required_channel_id):
|
||||
if (
|
||||
db_user.channel_subscription_verified
|
||||
and db_user.channel_subscription_verified_for == required_channel_id
|
||||
):
|
||||
return True
|
||||
|
||||
def translate(key: str, **kwargs) -> str:
|
||||
@@ -346,10 +344,11 @@ async def ensure_required_channel_subscription(
|
||||
)
|
||||
return True
|
||||
|
||||
keyboard = (get_channel_subscription_keyboard(
|
||||
current_lang, i18n, settings.REQUIRED_CHANNEL_LINK
|
||||
keyboard = (
|
||||
get_channel_subscription_keyboard(current_lang, i18n, settings.REQUIRED_CHANNEL_LINK)
|
||||
if i18n
|
||||
else None
|
||||
)
|
||||
if i18n else None)
|
||||
|
||||
prompt_text = translate("channel_subscription_required")
|
||||
|
||||
@@ -379,28 +378,41 @@ async def ensure_required_channel_subscription(
|
||||
|
||||
|
||||
@router.message(CommandStart())
|
||||
@router.message(CommandStart(magic=F.args.regexp(r"^ref_((?:[uU][A-Za-z0-9]{9})|(?:[A-Za-z0-9]{9})|\d+)$").as_("ref_match")))
|
||||
@router.message(
|
||||
CommandStart(
|
||||
magic=F.args.regexp(r"^ref_((?:[uU][A-Za-z0-9]{9})|(?:[A-Za-z0-9]{9})|\d+)$").as_(
|
||||
"ref_match"
|
||||
)
|
||||
)
|
||||
)
|
||||
@router.message(CommandStart(magic=F.args.regexp(r"^promo_(\w+)$").as_("promo_match")))
|
||||
@router.message(CommandStart(magic=F.args.regexp(r"^admin_user_(\d+)$").as_("admin_user_match")))
|
||||
@router.message(CommandStart(magic=F.args.regexp(r"^page_ref$").as_("page_ref_match")))
|
||||
@router.message(CommandStart(magic=F.args.regexp(r"^(?!ref_|promo_|admin_user_|page_ref$|webapp_auth_)([A-Za-z0-9_\-]{2,64})$").as_("ad_param_match")))
|
||||
async def start_command_handler(message: types.Message,
|
||||
state: FSMContext,
|
||||
settings: Settings,
|
||||
i18n_data: dict,
|
||||
subscription_service: SubscriptionService,
|
||||
referral_service: ReferralService,
|
||||
session: AsyncSession,
|
||||
ref_match: Optional[re.Match] = None,
|
||||
promo_match: Optional[re.Match] = None,
|
||||
page_ref_match: Optional[re.Match] = None,
|
||||
ad_param_match: Optional[re.Match] = None,
|
||||
admin_user_match: Optional[re.Match] = None):
|
||||
@router.message(
|
||||
CommandStart(
|
||||
magic=F.args.regexp(
|
||||
r"^(?!ref_|promo_|admin_user_|page_ref$|webapp_auth_)([A-Za-z0-9_\-]{2,64})$"
|
||||
).as_("ad_param_match")
|
||||
)
|
||||
)
|
||||
async def start_command_handler(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
settings: Settings,
|
||||
i18n_data: dict,
|
||||
subscription_service: SubscriptionService,
|
||||
referral_service: ReferralService,
|
||||
session: AsyncSession,
|
||||
ref_match: Optional[re.Match] = None,
|
||||
promo_match: Optional[re.Match] = None,
|
||||
page_ref_match: Optional[re.Match] = None,
|
||||
ad_param_match: Optional[re.Match] = None,
|
||||
admin_user_match: Optional[re.Match] = None,
|
||||
):
|
||||
await state.clear()
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs
|
||||
) if i18n else key
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key
|
||||
|
||||
user = message.from_user
|
||||
user_id = user.id
|
||||
@@ -409,21 +421,17 @@ async def start_command_handler(message: types.Message,
|
||||
target_user_id = int(admin_user_match.group(1))
|
||||
target_user = await user_dal.get_user_by_id(session, target_user_id)
|
||||
if not target_user:
|
||||
await message.answer(
|
||||
_("admin_user_not_found", input=hd.quote(str(target_user_id)))
|
||||
)
|
||||
await message.answer(_("admin_user_not_found", input=hd.quote(str(target_user_id))))
|
||||
return
|
||||
|
||||
try:
|
||||
from bot.handlers.admin.user_management import (
|
||||
_send_with_profile_link_fallback,
|
||||
format_user_card,
|
||||
get_user_card_keyboard,
|
||||
_send_with_profile_link_fallback,
|
||||
)
|
||||
|
||||
referral_service = ReferralService(
|
||||
settings, subscription_service, message.bot, i18n
|
||||
)
|
||||
referral_service = ReferralService(settings, subscription_service, message.bot, i18n)
|
||||
user_card_text = await format_user_card(
|
||||
target_user,
|
||||
session,
|
||||
@@ -468,7 +476,8 @@ async def start_command_handler(message: types.Message,
|
||||
if settings.LEGACY_REFS:
|
||||
potential_referrer_id = int(raw_ref_value)
|
||||
if potential_referrer_id != user_id and await user_dal.get_user_by_id(
|
||||
session, potential_referrer_id):
|
||||
session, potential_referrer_id
|
||||
):
|
||||
referred_by_user_id = potential_referrer_id
|
||||
else:
|
||||
normalized_code = raw_ref_value.strip()
|
||||
@@ -476,8 +485,7 @@ async def start_command_handler(message: types.Message,
|
||||
normalized_code = normalized_code[1:]
|
||||
ref_user = None
|
||||
if normalized_code:
|
||||
ref_user = await user_dal.get_user_by_referral_code(
|
||||
session, normalized_code)
|
||||
ref_user = await user_dal.get_user_by_referral_code(session, normalized_code)
|
||||
if ref_user and ref_user.user_id != user_id:
|
||||
referred_by_user_id = ref_user.user_id
|
||||
elif promo_match:
|
||||
@@ -504,7 +512,7 @@ async def start_command_handler(message: types.Message,
|
||||
"last_name": sanitized_last_name,
|
||||
"language_code": current_lang,
|
||||
"referred_by_id": referred_by_user_id,
|
||||
"registration_date": datetime.now(timezone.utc)
|
||||
"registration_date": datetime.now(timezone.utc),
|
||||
}
|
||||
try:
|
||||
db_user, created = await user_dal.create_user(session, user_data_to_create)
|
||||
@@ -531,11 +539,13 @@ async def start_command_handler(message: types.Message,
|
||||
)
|
||||
if referred_by_user_id and referral_welcome_days > 0:
|
||||
try:
|
||||
referral_bonus_end_date = await subscription_service.extend_active_subscription_days(
|
||||
session,
|
||||
user_id,
|
||||
referral_welcome_days,
|
||||
reason="referral_welcome_bonus",
|
||||
referral_bonus_end_date = (
|
||||
await subscription_service.extend_active_subscription_days(
|
||||
session,
|
||||
user_id,
|
||||
referral_welcome_days,
|
||||
reason="referral_welcome_bonus",
|
||||
)
|
||||
)
|
||||
if referral_bonus_end_date:
|
||||
await session.commit()
|
||||
@@ -572,20 +582,18 @@ async def start_command_handler(message: types.Message,
|
||||
# Send notification about new user registration
|
||||
try:
|
||||
from bot.services.notification_service import NotificationService
|
||||
|
||||
notification_service = NotificationService(message.bot, settings, i18n)
|
||||
await notification_service.notify_new_user_registration(
|
||||
user_id=user_id,
|
||||
username=sanitized_username,
|
||||
first_name=sanitized_first_name,
|
||||
referred_by_id=referred_by_user_id
|
||||
referred_by_id=referred_by_user_id,
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to send new user notification: {e}")
|
||||
except Exception as e_create:
|
||||
|
||||
logging.error(
|
||||
f"Failed to add new user {user_id} to session: {e_create}",
|
||||
exc_info=True)
|
||||
logging.error(f"Failed to add new user {user_id} to session: {e_create}", exc_info=True)
|
||||
await message.answer(_("error_occurred_processing_request"))
|
||||
return
|
||||
else:
|
||||
@@ -612,22 +620,23 @@ async def start_command_handler(message: types.Message,
|
||||
try:
|
||||
await user_dal.update_user(session, user_id, update_payload)
|
||||
|
||||
logging.info(
|
||||
f"Updated existing user {user_id} in session: {update_payload}"
|
||||
)
|
||||
logging.info(f"Updated existing user {user_id} in session: {update_payload}")
|
||||
except Exception as e_update:
|
||||
|
||||
logging.error(
|
||||
f"Failed to update existing user {user_id} in session: {e_update}",
|
||||
exc_info=True)
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
# Attribute user to ad campaign if start param provided
|
||||
if ad_start_param:
|
||||
try:
|
||||
from db.dal import ad_dal as _ad_dal
|
||||
|
||||
campaign = await _ad_dal.get_campaign_by_start_param(session, ad_start_param)
|
||||
if campaign and campaign.is_active:
|
||||
await _ad_dal.ensure_attribution(session, user_id=user_id, campaign_id=campaign.ad_campaign_id)
|
||||
await _ad_dal.ensure_attribution(
|
||||
session, user_id=user_id, campaign_id=campaign.ad_campaign_id
|
||||
)
|
||||
await session.commit()
|
||||
except Exception as e_attr:
|
||||
logging.error(f"Failed to attribute user {user_id} to ad '{ad_start_param}': {e_attr}")
|
||||
@@ -636,14 +645,12 @@ async def start_command_handler(message: types.Message,
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not await ensure_required_channel_subscription(message, settings, i18n,
|
||||
current_lang, session,
|
||||
db_user):
|
||||
if not await ensure_required_channel_subscription(
|
||||
message, settings, i18n, current_lang, session, db_user
|
||||
):
|
||||
return
|
||||
|
||||
open_referral_page_for_existing_user = (
|
||||
should_open_referral_from_start and is_existing_user
|
||||
)
|
||||
open_referral_page_for_existing_user = should_open_referral_from_start and is_existing_user
|
||||
|
||||
# Send welcome message if not disabled
|
||||
if not settings.DISABLE_WELCOME_MESSAGE and not open_referral_page_for_existing_user:
|
||||
@@ -653,6 +660,7 @@ async def start_command_handler(message: types.Message,
|
||||
if promo_code_to_apply:
|
||||
try:
|
||||
from bot.services.promo_code_service import PromoCodeService
|
||||
|
||||
promo_code_service = PromoCodeService(settings, subscription_service, message.bot, i18n)
|
||||
|
||||
success, result = await promo_code_service.apply_promo_code(
|
||||
@@ -664,7 +672,9 @@ async def start_command_handler(message: types.Message,
|
||||
logging.info(f"Auto-applied promo code '{promo_code_to_apply}' for user {user_id}")
|
||||
|
||||
# Get updated subscription details
|
||||
active = await subscription_service.get_active_subscription_details(session, user_id)
|
||||
active = await subscription_service.get_active_subscription_details(
|
||||
session, user_id
|
||||
)
|
||||
config_link_display = active.get("config_link") if active else None
|
||||
connect_button_url = active.get("connect_button_url") if active else None
|
||||
config_link_text = config_link_display or _("config_link_not_available")
|
||||
@@ -673,11 +683,14 @@ async def start_command_handler(message: types.Message,
|
||||
|
||||
promo_success_text = _(
|
||||
"promo_code_applied_success_full",
|
||||
end_date=(new_end_date.strftime("%d.%m.%Y %H:%M:%S") if new_end_date else "N/A"),
|
||||
end_date=(
|
||||
new_end_date.strftime("%d.%m.%Y %H:%M:%S") if new_end_date else "N/A"
|
||||
),
|
||||
config_link=config_link_text,
|
||||
)
|
||||
|
||||
from bot.keyboards.inline.user_keyboards import get_connect_and_main_keyboard
|
||||
|
||||
await message.answer(
|
||||
promo_success_text,
|
||||
reply_markup=get_connect_and_main_keyboard(
|
||||
@@ -687,75 +700,76 @@ async def start_command_handler(message: types.Message,
|
||||
config_link_display,
|
||||
connect_button_url=connect_button_url,
|
||||
),
|
||||
parse_mode="HTML"
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
# Don't show main menu if promo was successfully applied
|
||||
return
|
||||
else:
|
||||
await session.commit()
|
||||
logging.warning(f"Failed to auto-apply promo code '{promo_code_to_apply}' for user {user_id}: {result}")
|
||||
logging.warning(
|
||||
f"Failed to auto-apply promo code '{promo_code_to_apply}' for user {user_id}: {result}"
|
||||
)
|
||||
await message.answer(str(result), parse_mode="HTML")
|
||||
# Continue to show main menu if promo failed
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error auto-applying promo code '{promo_code_to_apply}' for user {user_id}: {e}")
|
||||
logging.error(
|
||||
f"Error auto-applying promo code '{promo_code_to_apply}' for user {user_id}: {e}"
|
||||
)
|
||||
await session.rollback()
|
||||
|
||||
if open_referral_page_for_existing_user:
|
||||
from . import referral as user_referral_handlers
|
||||
|
||||
await user_referral_handlers.referral_command_handler(
|
||||
message, settings, i18n_data, referral_service, message.bot, session
|
||||
)
|
||||
return
|
||||
|
||||
await send_main_menu(message,
|
||||
settings,
|
||||
i18n_data,
|
||||
subscription_service,
|
||||
session,
|
||||
is_edit=False)
|
||||
await send_main_menu(message, settings, i18n_data, subscription_service, session, is_edit=False)
|
||||
|
||||
|
||||
@router.message(Command("tg"))
|
||||
async def tg_interface_command_handler(message: types.Message,
|
||||
state: FSMContext,
|
||||
settings: Settings,
|
||||
i18n_data: dict,
|
||||
subscription_service: SubscriptionService,
|
||||
session: AsyncSession):
|
||||
async def tg_interface_command_handler(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
settings: Settings,
|
||||
i18n_data: dict,
|
||||
subscription_service: SubscriptionService,
|
||||
session: AsyncSession,
|
||||
):
|
||||
await state.clear()
|
||||
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
db_user = await user_dal.get_user_by_id(session, message.from_user.id)
|
||||
if not await ensure_required_channel_subscription(message, settings, i18n,
|
||||
current_lang, session,
|
||||
db_user):
|
||||
if not await ensure_required_channel_subscription(
|
||||
message, settings, i18n, current_lang, session, db_user
|
||||
):
|
||||
return
|
||||
|
||||
await send_bot_interface_menu(message,
|
||||
settings,
|
||||
i18n_data,
|
||||
subscription_service,
|
||||
session,
|
||||
is_edit=False)
|
||||
await send_bot_interface_menu(
|
||||
message, settings, i18n_data, subscription_service, session, is_edit=False
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "channel_subscription:verify")
|
||||
async def verify_channel_subscription_callback(
|
||||
callback: types.CallbackQuery,
|
||||
settings: Settings,
|
||||
i18n_data: dict,
|
||||
subscription_service: SubscriptionService,
|
||||
session: AsyncSession):
|
||||
callback: types.CallbackQuery,
|
||||
settings: Settings,
|
||||
i18n_data: dict,
|
||||
subscription_service: SubscriptionService,
|
||||
session: AsyncSession,
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
|
||||
db_user = await user_dal.get_user_by_id(session, callback.from_user.id)
|
||||
|
||||
verified = await ensure_required_channel_subscription(
|
||||
callback, settings, i18n, current_lang, session, db_user)
|
||||
callback, settings, i18n, current_lang, session, db_user
|
||||
)
|
||||
if not verified:
|
||||
return
|
||||
|
||||
@@ -769,15 +783,13 @@ async def verify_channel_subscription_callback(
|
||||
_ = lambda key, **kwargs: key
|
||||
|
||||
if not settings.DISABLE_WELCOME_MESSAGE:
|
||||
welcome_text = _(key="welcome",
|
||||
user_name=hd.quote(callback.from_user.full_name))
|
||||
welcome_text = _(key="welcome", user_name=hd.quote(callback.from_user.full_name))
|
||||
if callback.message:
|
||||
await callback.message.answer(welcome_text)
|
||||
else:
|
||||
fallback_bot: Optional[Bot] = getattr(callback, "bot", None)
|
||||
if fallback_bot:
|
||||
await fallback_bot.send_message(callback.from_user.id,
|
||||
welcome_text)
|
||||
await fallback_bot.send_message(callback.from_user.id, welcome_text)
|
||||
|
||||
try:
|
||||
await safe_answer_callback(
|
||||
@@ -788,12 +800,9 @@ async def verify_channel_subscription_callback(
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
await send_main_menu(callback,
|
||||
settings,
|
||||
i18n_data,
|
||||
subscription_service,
|
||||
session,
|
||||
is_edit=bool(callback.message))
|
||||
await send_main_menu(
|
||||
callback, settings, i18n_data, subscription_service, session, is_edit=bool(callback.message)
|
||||
)
|
||||
|
||||
|
||||
@router.message(Command("language"))
|
||||
@@ -806,8 +815,7 @@ async def language_command_handler(
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs
|
||||
) if i18n else key
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key
|
||||
|
||||
text_to_send = _(key="choose_language")
|
||||
reply_markup = get_language_selection_keyboard(
|
||||
@@ -816,8 +824,7 @@ async def language_command_handler(
|
||||
back_callback=back_callback,
|
||||
)
|
||||
|
||||
target_message_obj = event.message if isinstance(
|
||||
event, types.CallbackQuery) else event
|
||||
target_message_obj = event.message if isinstance(event, types.CallbackQuery) else event
|
||||
if not target_message_obj:
|
||||
if isinstance(event, types.CallbackQuery):
|
||||
await safe_answer_callback(
|
||||
@@ -830,21 +837,22 @@ async def language_command_handler(
|
||||
if isinstance(event, types.CallbackQuery):
|
||||
if event.message:
|
||||
try:
|
||||
await event.message.edit_text(text_to_send,
|
||||
reply_markup=reply_markup)
|
||||
await event.message.edit_text(text_to_send, reply_markup=reply_markup)
|
||||
except Exception:
|
||||
await target_message_obj.answer(text_to_send,
|
||||
reply_markup=reply_markup)
|
||||
await target_message_obj.answer(text_to_send, reply_markup=reply_markup)
|
||||
await safe_answer_callback(event)
|
||||
else:
|
||||
await target_message_obj.answer(text_to_send,
|
||||
reply_markup=reply_markup)
|
||||
await target_message_obj.answer(text_to_send, reply_markup=reply_markup)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("set_lang_"))
|
||||
async def select_language_callback_handler(
|
||||
callback: types.CallbackQuery, i18n_data: dict, settings: Settings,
|
||||
subscription_service: SubscriptionService, session: AsyncSession):
|
||||
callback: types.CallbackQuery,
|
||||
i18n_data: dict,
|
||||
settings: Settings,
|
||||
subscription_service: SubscriptionService,
|
||||
session: AsyncSession,
|
||||
):
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n or not callback.message:
|
||||
await safe_answer_callback(
|
||||
@@ -867,15 +875,12 @@ async def select_language_callback_handler(
|
||||
|
||||
user_id = callback.from_user.id
|
||||
try:
|
||||
updated = await user_dal.update_user_language(session, user_id,
|
||||
lang_code)
|
||||
updated = await user_dal.update_user_language(session, user_id, lang_code)
|
||||
if updated:
|
||||
|
||||
i18n_data["current_language"] = lang_code
|
||||
_ = lambda key, **kwargs: i18n.gettext(lang_code, key, **kwargs)
|
||||
await safe_answer_callback(callback, _(key="language_set_alert"))
|
||||
logging.info(
|
||||
f"User {user_id} language updated to {lang_code} in session.")
|
||||
logging.info(f"User {user_id} language updated to {lang_code} in session.")
|
||||
else:
|
||||
await safe_answer_callback(
|
||||
callback,
|
||||
@@ -884,43 +889,41 @@ async def select_language_callback_handler(
|
||||
)
|
||||
return
|
||||
except Exception as e_lang_update:
|
||||
|
||||
logging.error(
|
||||
f"Error updating lang for user {user_id}: {e_lang_update}",
|
||||
exc_info=True)
|
||||
logging.error(f"Error updating lang for user {user_id}: {e_lang_update}", exc_info=True)
|
||||
await safe_answer_callback(callback, "Error setting language.", show_alert=True)
|
||||
return
|
||||
if return_target == "bot":
|
||||
await send_bot_interface_menu(callback,
|
||||
settings,
|
||||
i18n_data,
|
||||
subscription_service,
|
||||
session,
|
||||
is_edit=True)
|
||||
await send_bot_interface_menu(
|
||||
callback, settings, i18n_data, subscription_service, session, is_edit=True
|
||||
)
|
||||
else:
|
||||
await send_main_menu(callback,
|
||||
settings,
|
||||
i18n_data,
|
||||
subscription_service,
|
||||
session,
|
||||
is_edit=True)
|
||||
await send_main_menu(
|
||||
callback, settings, i18n_data, subscription_service, session, is_edit=True
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("main_action:"))
|
||||
async def main_action_callback_handler(
|
||||
callback: types.CallbackQuery, state: FSMContext, settings: Settings,
|
||||
i18n_data: dict, bot: Bot, subscription_service: SubscriptionService,
|
||||
referral_service: ReferralService, panel_service: PanelApiService,
|
||||
promo_code_service: PromoCodeService, session: AsyncSession):
|
||||
callback: types.CallbackQuery,
|
||||
state: FSMContext,
|
||||
settings: Settings,
|
||||
i18n_data: dict,
|
||||
bot: Bot,
|
||||
subscription_service: SubscriptionService,
|
||||
referral_service: ReferralService,
|
||||
panel_service: PanelApiService,
|
||||
promo_code_service: PromoCodeService,
|
||||
session: AsyncSession,
|
||||
):
|
||||
action = callback.data.split(":")[1]
|
||||
user_id = callback.from_user.id
|
||||
|
||||
if action in {"back_to_main", "back_to_main_keep", "bot_interface"}:
|
||||
await state.clear()
|
||||
|
||||
from . import subscription as user_subscription_handlers
|
||||
from . import referral as user_referral_handlers
|
||||
from . import promo_user as user_promo_handlers
|
||||
from . import referral as user_referral_handlers
|
||||
from . import subscription as user_subscription_handlers
|
||||
from . import trial_handler as user_trial_handlers
|
||||
|
||||
if not callback.message:
|
||||
@@ -933,7 +936,8 @@ async def main_action_callback_handler(
|
||||
|
||||
if action == "subscribe":
|
||||
await user_subscription_handlers.display_subscription_options(
|
||||
callback, i18n_data, settings, session)
|
||||
callback, i18n_data, settings, session
|
||||
)
|
||||
elif action == "bot_subscribe":
|
||||
await user_subscription_handlers.display_subscription_options(
|
||||
callback,
|
||||
@@ -944,8 +948,8 @@ async def main_action_callback_handler(
|
||||
)
|
||||
elif action == "my_subscription":
|
||||
await user_subscription_handlers.my_subscription_command_handler(
|
||||
callback, i18n_data, settings, panel_service, subscription_service,
|
||||
session, bot)
|
||||
callback, i18n_data, settings, panel_service, subscription_service, session, bot
|
||||
)
|
||||
elif action == "bot_my_subscription":
|
||||
await user_subscription_handlers.my_subscription_command_handler(
|
||||
callback,
|
||||
@@ -959,11 +963,12 @@ async def main_action_callback_handler(
|
||||
)
|
||||
elif action == "my_devices":
|
||||
await user_subscription_handlers.my_devices_command_handler(
|
||||
callback, i18n_data, settings, panel_service, subscription_service,
|
||||
session, bot)
|
||||
callback, i18n_data, settings, panel_service, subscription_service, session, bot
|
||||
)
|
||||
elif action == "referral":
|
||||
await user_referral_handlers.referral_command_handler(
|
||||
callback, settings, i18n_data, referral_service, bot, session)
|
||||
callback, settings, i18n_data, referral_service, bot, session
|
||||
)
|
||||
elif action == "bot_referral":
|
||||
await user_referral_handlers.referral_command_handler(
|
||||
callback,
|
||||
@@ -976,7 +981,8 @@ async def main_action_callback_handler(
|
||||
)
|
||||
elif action == "apply_promo":
|
||||
await user_promo_handlers.prompt_promo_code_input(
|
||||
callback, state, i18n_data, settings, session)
|
||||
callback, state, i18n_data, settings, session
|
||||
)
|
||||
elif action == "bot_apply_promo":
|
||||
await user_promo_handlers.prompt_promo_code_input(
|
||||
callback,
|
||||
@@ -988,9 +994,9 @@ async def main_action_callback_handler(
|
||||
)
|
||||
elif action == "request_trial":
|
||||
await user_trial_handlers.request_trial_confirmation_handler(
|
||||
callback, settings, i18n_data, subscription_service, session)
|
||||
callback, settings, i18n_data, subscription_service, session
|
||||
)
|
||||
elif action == "language":
|
||||
|
||||
await language_command_handler(callback, i18n_data, settings)
|
||||
elif action == "bot_language":
|
||||
await language_command_handler(
|
||||
@@ -1000,16 +1006,12 @@ async def main_action_callback_handler(
|
||||
back_callback="main_action:bot_interface",
|
||||
)
|
||||
elif action == "bot_interface":
|
||||
await send_bot_interface_menu(callback,
|
||||
settings,
|
||||
i18n_data,
|
||||
subscription_service,
|
||||
session,
|
||||
is_edit=True)
|
||||
await send_bot_interface_menu(
|
||||
callback, settings, i18n_data, subscription_service, session, is_edit=True
|
||||
)
|
||||
elif action in {"info", "bot_info"}:
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
current_lang = i18n_data.get("current_language",
|
||||
settings.DEFAULT_LANGUAGE)
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
if not i18n:
|
||||
await safe_answer_callback(
|
||||
callback,
|
||||
@@ -1017,8 +1019,7 @@ async def main_action_callback_handler(
|
||||
show_alert=True,
|
||||
)
|
||||
return
|
||||
_ = lambda key, **kwargs: i18n.gettext(
|
||||
current_lang, key, **kwargs) if i18n else key
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key
|
||||
|
||||
privacy_url = settings.PRIVACY_POLICY_URL
|
||||
user_agreement_url = settings.USER_AGREEMENT_URL or settings.TERMS_OF_SERVICE_URL
|
||||
@@ -1043,30 +1044,23 @@ async def main_action_callback_handler(
|
||||
),
|
||||
)
|
||||
try:
|
||||
await callback.message.edit_text(_(key="info_links_message"),
|
||||
reply_markup=reply_markup)
|
||||
await callback.message.edit_text(_(key="info_links_message"), reply_markup=reply_markup)
|
||||
except Exception:
|
||||
await callback.message.answer(_(key="info_links_message"),
|
||||
reply_markup=reply_markup)
|
||||
await callback.message.answer(_(key="info_links_message"), reply_markup=reply_markup)
|
||||
await safe_answer_callback(callback)
|
||||
elif action == "back_to_main":
|
||||
await send_main_menu(callback,
|
||||
settings,
|
||||
i18n_data,
|
||||
subscription_service,
|
||||
session,
|
||||
is_edit=True)
|
||||
await send_main_menu(
|
||||
callback, settings, i18n_data, subscription_service, session, is_edit=True
|
||||
)
|
||||
elif action == "back_to_main_keep":
|
||||
await send_main_menu(callback,
|
||||
settings,
|
||||
i18n_data,
|
||||
subscription_service,
|
||||
session,
|
||||
is_edit=False)
|
||||
await send_main_menu(
|
||||
callback, settings, i18n_data, subscription_service, session, is_edit=False
|
||||
)
|
||||
else:
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
_ = lambda key, **kwargs: i18n.gettext(
|
||||
i18n_data.get("current_language"), key, **kwargs) if i18n else key
|
||||
_ = lambda key, **kwargs: (
|
||||
i18n.gettext(i18n_data.get("current_language"), key, **kwargs) if i18n else key
|
||||
)
|
||||
await safe_answer_callback(
|
||||
callback,
|
||||
_("main_menu_unknown_action"),
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
from aiogram import Router
|
||||
|
||||
from . import core
|
||||
from . import payments
|
||||
from . import payment_methods
|
||||
from . import core, payment_methods, payments
|
||||
|
||||
router = Router(name="user_subscription_router")
|
||||
|
||||
@@ -12,6 +10,8 @@ router.include_router(payments.router)
|
||||
router.include_router(payment_methods.router)
|
||||
|
||||
# Re-export commonly used entrypoints for backward compatibility
|
||||
from .core import display_subscription_options, my_subscription_command_handler, my_devices_command_handler # noqa: E402,F401
|
||||
|
||||
|
||||
from .core import ( # noqa: E402,F401
|
||||
display_subscription_options,
|
||||
my_devices_command_handler,
|
||||
my_subscription_command_handler,
|
||||
)
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import hashlib
|
||||
import html
|
||||
import logging
|
||||
from aiogram import Router, F, types, Bot
|
||||
from datetime import datetime
|
||||
from typing import Optional, Union
|
||||
|
||||
from aiogram import Bot, F, Router, types
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, WebAppInfo
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from typing import Optional, Union
|
||||
from datetime import datetime
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
|
||||
from config.settings import Settings
|
||||
from bot.keyboards.inline.user_keyboards import (
|
||||
get_subscription_options_keyboard,
|
||||
get_back_to_main_menu_markup,
|
||||
get_autorenew_confirm_keyboard,
|
||||
get_tariff_catalog_keyboard,
|
||||
get_tariff_periods_keyboard,
|
||||
get_tariff_packages_keyboard,
|
||||
get_payment_method_keyboard,
|
||||
get_back_to_main_menu_markup,
|
||||
get_hwid_device_packages_keyboard,
|
||||
get_payment_method_keyboard,
|
||||
get_subscription_options_keyboard,
|
||||
get_tariff_catalog_keyboard,
|
||||
get_tariff_packages_keyboard,
|
||||
get_tariff_periods_keyboard,
|
||||
)
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
from bot.services.panel_api_service import PanelApiService
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
from bot.services.panel_api_service import PanelApiService
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
from config.settings import Settings
|
||||
from db.dal import subscription_dal, user_billing_dal
|
||||
from db.models import Subscription
|
||||
|
||||
@@ -55,7 +55,9 @@ def _has_multiple_enabled_tariffs(settings: Settings) -> bool:
|
||||
return len(_enabled_tariffs(settings)) > 1
|
||||
|
||||
|
||||
def _tariff_purchase_markup(tariff, current_lang: str, i18n: JsonI18n, settings: Settings) -> InlineKeyboardMarkup:
|
||||
def _tariff_purchase_markup(
|
||||
tariff, current_lang: str, i18n: JsonI18n, settings: Settings
|
||||
) -> InlineKeyboardMarkup:
|
||||
if tariff.billing_model == "period":
|
||||
return get_tariff_periods_keyboard(tariff, current_lang, i18n, settings)
|
||||
return get_tariff_packages_keyboard(tariff, tariff.traffic_packages.rub, current_lang, i18n)
|
||||
@@ -130,7 +132,11 @@ async def display_subscription_options(
|
||||
options = settings.subscription_options
|
||||
|
||||
if options:
|
||||
text_content = get_text("select_traffic_package") if traffic_mode else get_text("select_subscription_period")
|
||||
text_content = (
|
||||
get_text("select_traffic_package")
|
||||
if traffic_mode
|
||||
else get_text("select_subscription_period")
|
||||
)
|
||||
reply_markup = get_subscription_options_keyboard(
|
||||
options,
|
||||
currency_symbol_val,
|
||||
@@ -170,12 +176,16 @@ async def display_subscription_options(
|
||||
|
||||
|
||||
@router.callback_query(F.data == "main_action:subscribe")
|
||||
async def reshow_subscription_options_callback(callback: types.CallbackQuery, i18n_data: dict, settings: Settings, session: AsyncSession):
|
||||
async def reshow_subscription_options_callback(
|
||||
callback: types.CallbackQuery, i18n_data: dict, settings: Settings, session: AsyncSession
|
||||
):
|
||||
await display_subscription_options(callback, i18n_data, settings, session)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("tariff:select:"))
|
||||
async def select_tariff_callback(callback: types.CallbackQuery, i18n_data: dict, settings: Settings, session: AsyncSession):
|
||||
async def select_tariff_callback(
|
||||
callback: types.CallbackQuery, i18n_data: dict, settings: Settings, session: AsyncSession
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: JsonI18n = i18n_data.get("i18n_instance")
|
||||
get_text = lambda key, **kw: i18n.gettext(current_lang, key, **kw)
|
||||
@@ -196,7 +206,9 @@ async def select_tariff_callback(callback: types.CallbackQuery, i18n_data: dict,
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("tariff:period:"))
|
||||
async def select_tariff_period_callback(callback: types.CallbackQuery, i18n_data: dict, settings: Settings, session: AsyncSession):
|
||||
async def select_tariff_period_callback(
|
||||
callback: types.CallbackQuery, i18n_data: dict, settings: Settings, session: AsyncSession
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: JsonI18n = i18n_data.get("i18n_instance")
|
||||
get_text = lambda key, **kw: i18n.gettext(current_lang, key, **kw)
|
||||
@@ -227,7 +239,9 @@ async def select_tariff_period_callback(callback: types.CallbackQuery, i18n_data
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("tariff:package:"))
|
||||
async def select_tariff_package_callback(callback: types.CallbackQuery, i18n_data: dict, settings: Settings, session: AsyncSession):
|
||||
async def select_tariff_package_callback(
|
||||
callback: types.CallbackQuery, i18n_data: dict, settings: Settings, session: AsyncSession
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: JsonI18n = i18n_data.get("i18n_instance")
|
||||
get_text = lambda key, **kw: i18n.gettext(current_lang, key, **kw)
|
||||
@@ -238,12 +252,18 @@ async def select_tariff_package_callback(callback: types.CallbackQuery, i18n_dat
|
||||
_, _, tariff_key, gb_raw = callback.data.split(":", 3)
|
||||
tariff = config.require(tariff_key)
|
||||
gb = float(gb_raw)
|
||||
packages = tariff.traffic_packages.rub if tariff.billing_model == "traffic" else (config.topup_packages_for(tariff).rub if config.topup_packages_for(tariff) else [])
|
||||
packages = (
|
||||
tariff.traffic_packages.rub
|
||||
if tariff.billing_model == "traffic"
|
||||
else (config.topup_packages_for(tariff).rub if config.topup_packages_for(tariff) else [])
|
||||
)
|
||||
package = next((pkg for pkg in packages if float(pkg.gb) == gb), None)
|
||||
if not package:
|
||||
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||
return
|
||||
sale_mode = f"{'traffic_package' if tariff.billing_model == 'traffic' else 'topup'}@{tariff.key}"
|
||||
sale_mode = (
|
||||
f"{'traffic_package' if tariff.billing_model == 'traffic' else 'topup'}@{tariff.key}"
|
||||
)
|
||||
markup = get_payment_method_keyboard(
|
||||
gb,
|
||||
package.price,
|
||||
@@ -259,12 +279,20 @@ async def select_tariff_package_callback(callback: types.CallbackQuery, i18n_dat
|
||||
|
||||
|
||||
@router.callback_query(F.data == "tariff_topup:list")
|
||||
async def tariff_topup_list_callback(callback: types.CallbackQuery, i18n_data: dict, settings: Settings, subscription_service: SubscriptionService, session: AsyncSession):
|
||||
async def tariff_topup_list_callback(
|
||||
callback: types.CallbackQuery,
|
||||
i18n_data: dict,
|
||||
settings: Settings,
|
||||
subscription_service: SubscriptionService,
|
||||
session: AsyncSession,
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: JsonI18n = i18n_data.get("i18n_instance")
|
||||
get_text = lambda key, **kw: i18n.gettext(current_lang, key, **kw)
|
||||
config = settings.tariffs_config
|
||||
active = await subscription_service.get_active_subscription_details(session, callback.from_user.id)
|
||||
active = await subscription_service.get_active_subscription_details(
|
||||
session, callback.from_user.id
|
||||
)
|
||||
if not config or not active or not active.get("tariff_key") or not callback.message:
|
||||
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||
return
|
||||
@@ -291,14 +319,24 @@ async def tariff_topup_list_callback(callback: types.CallbackQuery, i18n_data: d
|
||||
callback_data=f"tariff:premium_package:{tariff.key}:{package.gb:g}",
|
||||
)
|
||||
)
|
||||
builder.row(InlineKeyboardButton(text=get_text("back_to_main_menu_button"), callback_data="main_action:my_subscription"))
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text=get_text("back_to_main_menu_button"), callback_data="main_action:my_subscription"
|
||||
)
|
||||
)
|
||||
|
||||
premium_lines = []
|
||||
carryover_lines = []
|
||||
if rub_packages or premium_packages:
|
||||
carryover_lines.append("Докупленный трафик не сгорает: сначала расходуется месячный лимит, затем докупленный остаток.")
|
||||
carryover_lines.append(
|
||||
"Докупленный трафик не сгорает: сначала расходуется месячный лимит, затем докупленный остаток."
|
||||
)
|
||||
if int(active.get("premium_limit_bytes") or 0) > 0:
|
||||
premium_left = max(0, int(active.get("premium_limit_bytes") or 0) - int(active.get("premium_used_bytes") or 0))
|
||||
premium_left = max(
|
||||
0,
|
||||
int(active.get("premium_limit_bytes") or 0)
|
||||
- int(active.get("premium_used_bytes") or 0),
|
||||
)
|
||||
labels = active.get("premium_node_labels") or active.get("premium_squad_labels") or []
|
||||
if labels:
|
||||
visible = [str(label) for label in labels[:8]]
|
||||
@@ -319,7 +357,9 @@ async def tariff_topup_list_callback(callback: types.CallbackQuery, i18n_data: d
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("tariff:premium_package:"))
|
||||
async def select_tariff_premium_package_callback(callback: types.CallbackQuery, i18n_data: dict, settings: Settings, session: AsyncSession):
|
||||
async def select_tariff_premium_package_callback(
|
||||
callback: types.CallbackQuery, i18n_data: dict, settings: Settings, session: AsyncSession
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: JsonI18n = i18n_data.get("i18n_instance")
|
||||
get_text = lambda key, **kw: i18n.gettext(current_lang, key, **kw)
|
||||
@@ -350,12 +390,20 @@ async def select_tariff_premium_package_callback(callback: types.CallbackQuery,
|
||||
|
||||
|
||||
@router.callback_query(F.data == "hwid_devices:list")
|
||||
async def hwid_devices_list_callback(callback: types.CallbackQuery, i18n_data: dict, settings: Settings, subscription_service: SubscriptionService, session: AsyncSession):
|
||||
async def hwid_devices_list_callback(
|
||||
callback: types.CallbackQuery,
|
||||
i18n_data: dict,
|
||||
settings: Settings,
|
||||
subscription_service: SubscriptionService,
|
||||
session: AsyncSession,
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: JsonI18n = i18n_data.get("i18n_instance")
|
||||
get_text = lambda key, **kw: i18n.gettext(current_lang, key, **kw)
|
||||
config = settings.tariffs_config
|
||||
active = await subscription_service.get_active_subscription_details(session, callback.from_user.id)
|
||||
active = await subscription_service.get_active_subscription_details(
|
||||
session, callback.from_user.id
|
||||
)
|
||||
if not config or not active or not active.get("tariff_key") or not callback.message:
|
||||
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||
return
|
||||
@@ -381,7 +429,9 @@ async def hwid_devices_list_callback(callback: types.CallbackQuery, i18n_data: d
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("hwid_devices:package:"))
|
||||
async def hwid_devices_package_callback(callback: types.CallbackQuery, i18n_data: dict, settings: Settings, session: AsyncSession):
|
||||
async def hwid_devices_package_callback(
|
||||
callback: types.CallbackQuery, i18n_data: dict, settings: Settings, session: AsyncSession
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: JsonI18n = i18n_data.get("i18n_instance")
|
||||
get_text = lambda key, **kw: i18n.gettext(current_lang, key, **kw)
|
||||
@@ -393,7 +443,11 @@ async def hwid_devices_package_callback(callback: types.CallbackQuery, i18n_data
|
||||
tariff = config.require(tariff_key)
|
||||
count = int(count_raw)
|
||||
package = next(
|
||||
(pkg for pkg in (tariff.hwid_device_packages.rub if tariff.hwid_device_packages else []) if int(pkg.count) == count),
|
||||
(
|
||||
pkg
|
||||
for pkg in (tariff.hwid_device_packages.rub if tariff.hwid_device_packages else [])
|
||||
if int(pkg.count) == count
|
||||
),
|
||||
None,
|
||||
)
|
||||
if not package:
|
||||
@@ -409,34 +463,68 @@ async def hwid_devices_package_callback(callback: types.CallbackQuery, i18n_data
|
||||
settings,
|
||||
sale_mode=f"hwid_devices@{tariff.key}",
|
||||
)
|
||||
await callback.message.edit_text(get_text("choose_payment_method_hwid_devices"), reply_markup=markup)
|
||||
await callback.message.edit_text(
|
||||
get_text("choose_payment_method_hwid_devices"), reply_markup=markup
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data == "tariff_change:list")
|
||||
async def tariff_change_list_callback(callback: types.CallbackQuery, i18n_data: dict, settings: Settings, subscription_service: SubscriptionService, session: AsyncSession):
|
||||
async def tariff_change_list_callback(
|
||||
callback: types.CallbackQuery,
|
||||
i18n_data: dict,
|
||||
settings: Settings,
|
||||
subscription_service: SubscriptionService,
|
||||
session: AsyncSession,
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: JsonI18n = i18n_data.get("i18n_instance")
|
||||
config = settings.tariffs_config
|
||||
active = await subscription_service.get_active_subscription_details(session, callback.from_user.id)
|
||||
active = await subscription_service.get_active_subscription_details(
|
||||
session, callback.from_user.id
|
||||
)
|
||||
if not config or not active or not callback.message:
|
||||
await callback.answer("Error", show_alert=True)
|
||||
return
|
||||
if len(config.enabled_tariffs) <= 1:
|
||||
await callback.answer("Смена тарифа недоступна: сейчас включен только один тариф.", show_alert=True)
|
||||
await callback.answer(
|
||||
"Смена тарифа недоступна: сейчас включен только один тариф.", show_alert=True
|
||||
)
|
||||
return
|
||||
rows = []
|
||||
for tariff in config.enabled_tariffs:
|
||||
if tariff.key == active.get("tariff_key"):
|
||||
continue
|
||||
rows.append([InlineKeyboardButton(text=tariff.name(current_lang), callback_data=f"tariff_change:select:{tariff.key}")])
|
||||
rows.append([InlineKeyboardButton(text=i18n.gettext(current_lang, "back_to_main_menu_button"), callback_data="main_action:my_subscription")])
|
||||
await callback.message.edit_text("Выберите тариф", reply_markup=InlineKeyboardMarkup(inline_keyboard=rows))
|
||||
rows.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=tariff.name(current_lang),
|
||||
callback_data=f"tariff_change:select:{tariff.key}",
|
||||
)
|
||||
]
|
||||
)
|
||||
rows.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=i18n.gettext(current_lang, "back_to_main_menu_button"),
|
||||
callback_data="main_action:my_subscription",
|
||||
)
|
||||
]
|
||||
)
|
||||
await callback.message.edit_text(
|
||||
"Выберите тариф", reply_markup=InlineKeyboardMarkup(inline_keyboard=rows)
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("tariff_change:select:"))
|
||||
async def tariff_change_select_callback(callback: types.CallbackQuery, i18n_data: dict, settings: Settings, subscription_service: SubscriptionService, session: AsyncSession):
|
||||
async def tariff_change_select_callback(
|
||||
callback: types.CallbackQuery,
|
||||
i18n_data: dict,
|
||||
settings: Settings,
|
||||
subscription_service: SubscriptionService,
|
||||
session: AsyncSession,
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: JsonI18n = i18n_data.get("i18n_instance")
|
||||
config = settings.tariffs_config
|
||||
@@ -445,32 +533,85 @@ async def tariff_change_select_callback(callback: types.CallbackQuery, i18n_data
|
||||
return
|
||||
tariff_key = callback.data.split(":", 2)[2]
|
||||
target = config.require(tariff_key)
|
||||
db_sub = await subscription_dal.get_active_subscription_by_user_id(session, callback.from_user.id)
|
||||
db_sub = await subscription_dal.get_active_subscription_by_user_id(
|
||||
session, callback.from_user.id
|
||||
)
|
||||
if not db_sub:
|
||||
await callback.answer("Error", show_alert=True)
|
||||
return
|
||||
options = subscription_service.calculate_tariff_switch_options(db_sub, target)
|
||||
rows = []
|
||||
if options["mode"] == "period_to_period":
|
||||
rows.append([InlineKeyboardButton(text=f"Без доплаты, дней станет {options['recalc_days']}", callback_data=f"tariff_change:confirm_apply:{target.key}:recalc_days")])
|
||||
rows.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=f"Без доплаты, дней станет {options['recalc_days']}",
|
||||
callback_data=f"tariff_change:confirm_apply:{target.key}:recalc_days",
|
||||
)
|
||||
]
|
||||
)
|
||||
if options.get("paid_diff_rub", 0) > 0:
|
||||
rows.append([InlineKeyboardButton(text=f"Доплатить {options['paid_diff_rub']} RUB", callback_data=f"tariff_change:confirm_pay:{target.key}:{options['paid_diff_rub']}")])
|
||||
rows.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=f"Доплатить {options['paid_diff_rub']} RUB",
|
||||
callback_data=f"tariff_change:confirm_pay:{target.key}:{options['paid_diff_rub']}",
|
||||
)
|
||||
]
|
||||
)
|
||||
elif options["mode"] == "period_to_traffic":
|
||||
rows.append([InlineKeyboardButton(text=f"Перейти без доплаты, получить {options['converted_gb']} GB", callback_data=f"tariff_change:confirm_apply:{target.key}:convert_days_to_gb")])
|
||||
rows.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=f"Перейти без доплаты, получить {options['converted_gb']} GB",
|
||||
callback_data=f"tariff_change:confirm_apply:{target.key}:convert_days_to_gb",
|
||||
)
|
||||
]
|
||||
)
|
||||
for package in target.traffic_packages.rub:
|
||||
rows.append([InlineKeyboardButton(text=f"+ {package.gb:g} GB за {package.price:g} RUB", callback_data=f"tariff:package:{target.key}:{package.gb:g}")])
|
||||
rows.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=f"+ {package.gb:g} GB за {package.price:g} RUB",
|
||||
callback_data=f"tariff:package:{target.key}:{package.gb:g}",
|
||||
)
|
||||
]
|
||||
)
|
||||
else:
|
||||
for months in target.enabled_periods:
|
||||
price = target.period_price(months, "rub")
|
||||
if price:
|
||||
rows.append([InlineKeyboardButton(text=f"{months} мес. за {price:g} RUB", callback_data=f"tariff:period:{target.key}:{months}")])
|
||||
rows.append([InlineKeyboardButton(text=i18n.gettext(current_lang, "back_to_main_menu_button"), callback_data="tariff_change:list")])
|
||||
await callback.message.edit_text(f"{target.name(current_lang)}\n{target.description(current_lang)}".strip(), reply_markup=InlineKeyboardMarkup(inline_keyboard=rows))
|
||||
rows.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=f"{months} мес. за {price:g} RUB",
|
||||
callback_data=f"tariff:period:{target.key}:{months}",
|
||||
)
|
||||
]
|
||||
)
|
||||
rows.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=i18n.gettext(current_lang, "back_to_main_menu_button"),
|
||||
callback_data="tariff_change:list",
|
||||
)
|
||||
]
|
||||
)
|
||||
await callback.message.edit_text(
|
||||
f"{target.name(current_lang)}\n{target.description(current_lang)}".strip(),
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=rows),
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("tariff_change:confirm_apply:"))
|
||||
async def tariff_change_confirm_apply_callback(callback: types.CallbackQuery, i18n_data: dict, settings: Settings, subscription_service: SubscriptionService, session: AsyncSession):
|
||||
async def tariff_change_confirm_apply_callback(
|
||||
callback: types.CallbackQuery,
|
||||
i18n_data: dict,
|
||||
settings: Settings,
|
||||
subscription_service: SubscriptionService,
|
||||
session: AsyncSession,
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: JsonI18n = i18n_data.get("i18n_instance")
|
||||
config = settings.tariffs_config
|
||||
@@ -479,7 +620,9 @@ async def tariff_change_confirm_apply_callback(callback: types.CallbackQuery, i1
|
||||
return
|
||||
_, _, tariff_key, mode = callback.data.split(":", 3)
|
||||
target = config.require(tariff_key)
|
||||
db_sub = await subscription_dal.get_active_subscription_by_user_id(session, callback.from_user.id)
|
||||
db_sub = await subscription_dal.get_active_subscription_by_user_id(
|
||||
session, callback.from_user.id
|
||||
)
|
||||
if not db_sub:
|
||||
await callback.answer("Error", show_alert=True)
|
||||
return
|
||||
@@ -491,8 +634,17 @@ async def tariff_change_confirm_apply_callback(callback: types.CallbackQuery, i1
|
||||
else:
|
||||
action_text = "тариф будет изменен без доплаты"
|
||||
rows = [
|
||||
[InlineKeyboardButton(text="✅ Подтвердить", callback_data=f"tariff_change:apply:{target.key}:{mode}")],
|
||||
[InlineKeyboardButton(text=i18n.gettext(current_lang, "back_to_main_menu_button"), callback_data=f"tariff_change:select:{target.key}")],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="✅ Подтвердить", callback_data=f"tariff_change:apply:{target.key}:{mode}"
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=i18n.gettext(current_lang, "back_to_main_menu_button"),
|
||||
callback_data=f"tariff_change:select:{target.key}",
|
||||
)
|
||||
],
|
||||
]
|
||||
await callback.message.edit_text(
|
||||
f"Подтвердите смену тарифа\n\nНовый тариф: {target.name(current_lang)}\nИзменение: {action_text}",
|
||||
@@ -502,7 +654,9 @@ async def tariff_change_confirm_apply_callback(callback: types.CallbackQuery, i1
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("tariff_change:confirm_pay:"))
|
||||
async def tariff_change_confirm_pay_callback(callback: types.CallbackQuery, i18n_data: dict, settings: Settings):
|
||||
async def tariff_change_confirm_pay_callback(
|
||||
callback: types.CallbackQuery, i18n_data: dict, settings: Settings
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: JsonI18n = i18n_data.get("i18n_instance")
|
||||
config = settings.tariffs_config
|
||||
@@ -512,8 +666,18 @@ async def tariff_change_confirm_pay_callback(callback: types.CallbackQuery, i18n
|
||||
_, _, tariff_key, amount_raw = callback.data.split(":", 3)
|
||||
target = config.require(tariff_key)
|
||||
rows = [
|
||||
[InlineKeyboardButton(text="✅ Подтвердить и оплатить", callback_data=f"tariff_change:pay:{target.key}:{amount_raw}")],
|
||||
[InlineKeyboardButton(text=i18n.gettext(current_lang, "back_to_main_menu_button"), callback_data=f"tariff_change:select:{target.key}")],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="✅ Подтвердить и оплатить",
|
||||
callback_data=f"tariff_change:pay:{target.key}:{amount_raw}",
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=i18n.gettext(current_lang, "back_to_main_menu_button"),
|
||||
callback_data=f"tariff_change:select:{target.key}",
|
||||
)
|
||||
],
|
||||
]
|
||||
await callback.message.edit_text(
|
||||
f"Подтвердите смену тарифа\n\nНовый тариф: {target.name(current_lang)}\nБудет создана оплата на {amount_raw} RUB.",
|
||||
@@ -523,19 +687,37 @@ async def tariff_change_confirm_pay_callback(callback: types.CallbackQuery, i18n
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("tariff_change:apply:"))
|
||||
async def tariff_change_apply_callback(callback: types.CallbackQuery, i18n_data: dict, settings: Settings, subscription_service: SubscriptionService, session: AsyncSession):
|
||||
async def tariff_change_apply_callback(
|
||||
callback: types.CallbackQuery,
|
||||
i18n_data: dict,
|
||||
settings: Settings,
|
||||
subscription_service: SubscriptionService,
|
||||
session: AsyncSession,
|
||||
):
|
||||
_, _, tariff_key, mode = callback.data.split(":", 3)
|
||||
result = await subscription_service.switch_tariff_without_payment(session, callback.from_user.id, tariff_key, mode)
|
||||
result = await subscription_service.switch_tariff_without_payment(
|
||||
session, callback.from_user.id, tariff_key, mode
|
||||
)
|
||||
if result:
|
||||
await session.commit()
|
||||
await callback.answer("Готово", show_alert=True)
|
||||
await my_subscription_command_handler(callback, i18n_data, settings, subscription_service.panel_service, subscription_service, session, callback.bot)
|
||||
await my_subscription_command_handler(
|
||||
callback,
|
||||
i18n_data,
|
||||
settings,
|
||||
subscription_service.panel_service,
|
||||
subscription_service,
|
||||
session,
|
||||
callback.bot,
|
||||
)
|
||||
else:
|
||||
await callback.answer("Error", show_alert=True)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("tariff_change:pay:"))
|
||||
async def tariff_change_pay_callback(callback: types.CallbackQuery, i18n_data: dict, settings: Settings, session: AsyncSession):
|
||||
async def tariff_change_pay_callback(
|
||||
callback: types.CallbackQuery, i18n_data: dict, settings: Settings, session: AsyncSession
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: JsonI18n = i18n_data.get("i18n_instance")
|
||||
_, _, tariff_key, amount_raw = callback.data.split(":", 3)
|
||||
@@ -613,6 +795,7 @@ async def my_subscription_command_handler(
|
||||
config_link_display = active.get("config_link")
|
||||
connect_button_url = active.get("connect_button_url")
|
||||
config_link_value = config_link_display or get_text("config_link_not_available")
|
||||
|
||||
def _fmt_gb(val: Optional[float]) -> str:
|
||||
if val is None:
|
||||
return get_text("traffic_na")
|
||||
@@ -623,6 +806,7 @@ async def my_subscription_command_handler(
|
||||
except Exception:
|
||||
pass
|
||||
return str(val)
|
||||
|
||||
def _format_traffic_period(strategy: Optional[str]) -> Optional[str]:
|
||||
if not strategy:
|
||||
return None
|
||||
@@ -639,14 +823,18 @@ async def my_subscription_command_handler(
|
||||
def _format_used_with_period(used_display: str, period_label: Optional[str]) -> str:
|
||||
if not period_label:
|
||||
return used_display
|
||||
return get_text("traffic_used_with_period", traffic_used=used_display, traffic_period=period_label)
|
||||
return get_text(
|
||||
"traffic_used_with_period", traffic_used=used_display, traffic_period=period_label
|
||||
)
|
||||
|
||||
period_label = _format_traffic_period(active.get("traffic_limit_strategy"))
|
||||
period_label = period_label or get_text("traffic_period_unknown")
|
||||
|
||||
if traffic_mode:
|
||||
limit_display = _fmt_gb(active.get("traffic_limit_bytes"))
|
||||
used_display = _format_used_with_period(_fmt_gb(active.get("traffic_used_bytes")), period_label)
|
||||
used_display = _format_used_with_period(
|
||||
_fmt_gb(active.get("traffic_used_bytes")), period_label
|
||||
)
|
||||
remaining_display = get_text("traffic_na")
|
||||
try:
|
||||
limit_val = active.get("traffic_limit_bytes") or 0
|
||||
@@ -677,10 +865,16 @@ async def my_subscription_command_handler(
|
||||
days_left=max(0, days_left),
|
||||
status=active.get("status_from_panel", get_text("status_active")).capitalize(),
|
||||
config_link=config_link_value,
|
||||
traffic_limit=(f"{active['traffic_limit_bytes'] / 2**30:.2f} GB" if active.get("traffic_limit_bytes") else get_text("traffic_unlimited")),
|
||||
traffic_limit=(
|
||||
f"{active['traffic_limit_bytes'] / 2**30:.2f} GB"
|
||||
if active.get("traffic_limit_bytes")
|
||||
else get_text("traffic_unlimited")
|
||||
),
|
||||
traffic_used=(
|
||||
_format_used_with_period(
|
||||
f"{active['traffic_used_bytes'] / 2**30:.2f} GB" if active.get("traffic_used_bytes") is not None else get_text("traffic_na"),
|
||||
f"{active['traffic_used_bytes'] / 2**30:.2f} GB"
|
||||
if active.get("traffic_used_bytes") is not None
|
||||
else get_text("traffic_na"),
|
||||
period_label,
|
||||
)
|
||||
),
|
||||
@@ -721,26 +915,32 @@ async def my_subscription_command_handler(
|
||||
)
|
||||
kb = base_markup.inline_keyboard
|
||||
try:
|
||||
local_sub = await subscription_dal.get_active_subscription_by_user_id(session, event.from_user.id)
|
||||
local_sub = await subscription_dal.get_active_subscription_by_user_id(
|
||||
session, event.from_user.id
|
||||
)
|
||||
# Build rows to prepend above the base "back" markup
|
||||
prepend_rows = []
|
||||
|
||||
# 1) Connect button: prefer the actual subscription URL; fall back to mini-app
|
||||
cfg_link_val = connect_button_url or config_link_display
|
||||
if cfg_link_val:
|
||||
prepend_rows.append([
|
||||
InlineKeyboardButton(
|
||||
text=get_text("connect_button"),
|
||||
url=cfg_link_val,
|
||||
)
|
||||
])
|
||||
prepend_rows.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=get_text("connect_button"),
|
||||
url=cfg_link_val,
|
||||
)
|
||||
]
|
||||
)
|
||||
elif settings.SUBSCRIPTION_MINI_APP_URL:
|
||||
prepend_rows.append([
|
||||
InlineKeyboardButton(
|
||||
text=get_text("connect_button"),
|
||||
web_app=WebAppInfo(url=settings.SUBSCRIPTION_MINI_APP_URL),
|
||||
)
|
||||
])
|
||||
prepend_rows.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=get_text("connect_button"),
|
||||
web_app=WebAppInfo(url=settings.SUBSCRIPTION_MINI_APP_URL),
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
if settings.MY_DEVICES_SECTION_ENABLED:
|
||||
max_devices_value = active.get("max_devices")
|
||||
@@ -786,47 +986,69 @@ async def my_subscription_command_handler(
|
||||
current_devices=current_devices_display,
|
||||
max_devices=max_devices_display,
|
||||
)
|
||||
prepend_rows.append([
|
||||
InlineKeyboardButton(
|
||||
text=devices_button_text,
|
||||
callback_data="main_action:my_devices",
|
||||
)
|
||||
])
|
||||
prepend_rows.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=devices_button_text,
|
||||
callback_data="main_action:my_devices",
|
||||
)
|
||||
]
|
||||
)
|
||||
if settings.tariffs_config and local_sub and local_sub.tariff_key:
|
||||
try:
|
||||
tariff_for_devices = settings.tariffs_config.require(local_sub.tariff_key)
|
||||
if tariff_for_devices.hwid_device_packages and tariff_for_devices.hwid_device_packages.rub:
|
||||
prepend_rows.append([
|
||||
InlineKeyboardButton(
|
||||
text=get_text("buy_hwid_devices_menu_button"),
|
||||
callback_data="hwid_devices:list",
|
||||
)
|
||||
])
|
||||
if (
|
||||
tariff_for_devices.hwid_device_packages
|
||||
and tariff_for_devices.hwid_device_packages.rub
|
||||
):
|
||||
prepend_rows.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=get_text("buy_hwid_devices_menu_button"),
|
||||
callback_data="hwid_devices:list",
|
||||
)
|
||||
]
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 2) Auto-renew toggle (YooKassa only)
|
||||
if not traffic_mode and local_sub and local_sub.provider == "yookassa" and settings.yookassa_autopayments_active:
|
||||
if (
|
||||
not traffic_mode
|
||||
and local_sub
|
||||
and local_sub.provider == "yookassa"
|
||||
and settings.yookassa_autopayments_active
|
||||
):
|
||||
toggle_text = (
|
||||
get_text("autorenew_disable_button") if local_sub.auto_renew_enabled else get_text("autorenew_enable_button")
|
||||
get_text("autorenew_disable_button")
|
||||
if local_sub.auto_renew_enabled
|
||||
else get_text("autorenew_enable_button")
|
||||
)
|
||||
prepend_rows.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=toggle_text,
|
||||
callback_data=f"toggle_autorenew:{local_sub.subscription_id}:{1 if not local_sub.auto_renew_enabled else 0}",
|
||||
)
|
||||
]
|
||||
)
|
||||
prepend_rows.append([
|
||||
InlineKeyboardButton(
|
||||
text=toggle_text,
|
||||
callback_data=f"toggle_autorenew:{local_sub.subscription_id}:{1 if not local_sub.auto_renew_enabled else 0}",
|
||||
)
|
||||
])
|
||||
|
||||
# 3) Payment methods management (when autopayments enabled)
|
||||
if not traffic_mode and settings.yookassa_autopayments_active:
|
||||
prepend_rows.append([
|
||||
InlineKeyboardButton(text=get_text("payment_methods_manage_button"), callback_data="pm:manage")
|
||||
])
|
||||
prepend_rows.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=get_text("payment_methods_manage_button"), callback_data="pm:manage"
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
if settings.tariffs_config and local_sub and local_sub.tariff_key:
|
||||
tariff_actions = []
|
||||
if _has_multiple_enabled_tariffs(settings):
|
||||
tariff_actions.append(InlineKeyboardButton(text="Сменить тариф", callback_data="tariff_change:list"))
|
||||
tariff_actions.append(
|
||||
InlineKeyboardButton(text="Сменить тариф", callback_data="tariff_change:list")
|
||||
)
|
||||
try:
|
||||
tariff = settings.tariffs_config.require(local_sub.tariff_key)
|
||||
topup_packages = settings.tariffs_config.topup_packages_for(tariff)
|
||||
@@ -837,7 +1059,9 @@ async def my_subscription_command_handler(
|
||||
except Exception:
|
||||
has_topup_packages = False
|
||||
if has_topup_packages:
|
||||
tariff_actions.append(InlineKeyboardButton(text="Докупить трафик", callback_data="tariff_topup:list"))
|
||||
tariff_actions.append(
|
||||
InlineKeyboardButton(text="Докупить трафик", callback_data="tariff_topup:list")
|
||||
)
|
||||
if tariff_actions:
|
||||
prepend_rows.append(tariff_actions)
|
||||
|
||||
@@ -853,7 +1077,9 @@ async def my_subscription_command_handler(
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
await event.message.edit_text(text, reply_markup=markup, parse_mode="HTML", disable_web_page_preview=True)
|
||||
await event.message.edit_text(
|
||||
text, reply_markup=markup, parse_mode="HTML", disable_web_page_preview=True
|
||||
)
|
||||
except Exception:
|
||||
await bot.send_message(
|
||||
chat_id=target.chat.id,
|
||||
@@ -863,7 +1089,9 @@ async def my_subscription_command_handler(
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
else:
|
||||
await target.answer(text, reply_markup=markup, parse_mode="HTML", disable_web_page_preview=True)
|
||||
await target.answer(
|
||||
text, reply_markup=markup, parse_mode="HTML", disable_web_page_preview=True
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "main_action:my_devices")
|
||||
@@ -942,46 +1170,79 @@ async def my_devices_command_handler(
|
||||
devices_list = []
|
||||
current_devices = len(devices_list_raw)
|
||||
for index, device in enumerate(devices_list_raw, start=1):
|
||||
device_model = device.get('deviceModel') or None
|
||||
platform = device.get('platform') or None
|
||||
user_agent = device.get('userAgent') or None
|
||||
os_version = device.get('osVersion') or None
|
||||
created_at = device.get('createdAt')
|
||||
hwid = device.get('hwid')
|
||||
device_model = device.get("deviceModel") or None
|
||||
platform = device.get("platform") or None
|
||||
user_agent = device.get("userAgent") or None
|
||||
os_version = device.get("osVersion") or None
|
||||
created_at = device.get("createdAt")
|
||||
hwid = device.get("hwid")
|
||||
try:
|
||||
created_at_str = datetime.fromisoformat(created_at).strftime("%d.%m.%Y %H:%M") if created_at else "-"
|
||||
created_at_str = (
|
||||
datetime.fromisoformat(created_at).strftime("%d.%m.%Y %H:%M")
|
||||
if created_at
|
||||
else "-"
|
||||
)
|
||||
except Exception:
|
||||
created_at_str = str(created_at)
|
||||
|
||||
device_details = get_text("device_details", index=index, device_model=device_model, platform=platform, os_version=os_version, created_at_str=created_at_str, user_agent=user_agent, hwid=hwid)
|
||||
device_details = get_text(
|
||||
"device_details",
|
||||
index=index,
|
||||
device_model=device_model,
|
||||
platform=platform,
|
||||
os_version=os_version,
|
||||
created_at_str=created_at_str,
|
||||
user_agent=user_agent,
|
||||
hwid=hwid,
|
||||
)
|
||||
devices_list.append(device_details)
|
||||
|
||||
text = get_text("my_devices_details", devices="\n\n".join(devices_list), current_devices=current_devices, max_devices=max_devices_display)
|
||||
text = get_text(
|
||||
"my_devices_details",
|
||||
devices="\n\n".join(devices_list),
|
||||
current_devices=current_devices,
|
||||
max_devices=max_devices_display,
|
||||
)
|
||||
|
||||
base_markup = get_back_to_main_menu_markup(current_lang, i18n, callback_data="main_action:my_subscription")
|
||||
base_markup = get_back_to_main_menu_markup(
|
||||
current_lang, i18n, callback_data="main_action:my_subscription"
|
||||
)
|
||||
kb = base_markup.inline_keyboard
|
||||
|
||||
devices_kb = []
|
||||
if settings.tariffs_config and active.get("tariff_key") and max_devices_value != 0:
|
||||
try:
|
||||
tariff_for_devices = settings.tariffs_config.require(active["tariff_key"])
|
||||
if tariff_for_devices.hwid_device_packages and tariff_for_devices.hwid_device_packages.rub:
|
||||
devices_kb.append([
|
||||
InlineKeyboardButton(
|
||||
text=get_text("buy_hwid_devices_menu_button"),
|
||||
callback_data="hwid_devices:list",
|
||||
)
|
||||
])
|
||||
if (
|
||||
tariff_for_devices.hwid_device_packages
|
||||
and tariff_for_devices.hwid_device_packages.rub
|
||||
):
|
||||
devices_kb.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=get_text("buy_hwid_devices_menu_button"),
|
||||
callback_data="hwid_devices:list",
|
||||
)
|
||||
]
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
for index, device in enumerate(devices_list_raw, start=1):
|
||||
hwid = device.get('hwid')
|
||||
hwid = device.get("hwid")
|
||||
if not hwid:
|
||||
continue
|
||||
device_button_text = get_text("disconnect_device_button", hwid=_shorten_hwid_for_display(hwid), index=index)
|
||||
device_button_text = get_text(
|
||||
"disconnect_device_button", hwid=_shorten_hwid_for_display(hwid), index=index
|
||||
)
|
||||
hwid_token = _hwid_callback_token(hwid)
|
||||
|
||||
devices_kb.append([InlineKeyboardButton(text=device_button_text, callback_data=f"disconnect_device:{hwid_token}")])
|
||||
devices_kb.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=device_button_text, callback_data=f"disconnect_device:{hwid_token}"
|
||||
)
|
||||
]
|
||||
)
|
||||
kb = devices_kb + kb
|
||||
markup = InlineKeyboardMarkup(inline_keyboard=kb)
|
||||
|
||||
@@ -1028,7 +1289,9 @@ async def disconnect_device_handler(
|
||||
pass
|
||||
return
|
||||
|
||||
active = await subscription_service.get_active_subscription_details(session, callback.from_user.id)
|
||||
active = await subscription_service.get_active_subscription_details(
|
||||
session, callback.from_user.id
|
||||
)
|
||||
if not active or not active.get("user_id"):
|
||||
await callback.answer(get_text("subscription_not_active"), show_alert=True)
|
||||
return
|
||||
@@ -1064,7 +1327,9 @@ async def disconnect_device_handler(
|
||||
await callback.answer(get_text("device_disconnected"))
|
||||
except Exception:
|
||||
pass
|
||||
await my_devices_command_handler(callback, i18n_data, settings, panel_service, subscription_service, session, bot)
|
||||
await my_devices_command_handler(
|
||||
callback, i18n_data, settings, panel_service, subscription_service, session, bot
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("toggle_autorenew:"))
|
||||
@@ -1101,7 +1366,9 @@ async def toggle_autorenew_handler(
|
||||
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||
return
|
||||
if enable:
|
||||
has_saved_card = await user_billing_dal.user_has_saved_payment_method(session, callback.from_user.id)
|
||||
has_saved_card = await user_billing_dal.user_has_saved_payment_method(
|
||||
session, callback.from_user.id
|
||||
)
|
||||
if not has_saved_card:
|
||||
try:
|
||||
await callback.answer(get_text("autorenew_enable_requires_card"), show_alert=True)
|
||||
@@ -1110,7 +1377,9 @@ async def toggle_autorenew_handler(
|
||||
return
|
||||
|
||||
# Show confirmation popup and inline buttons
|
||||
confirm_text = get_text("autorenew_confirm_enable") if enable else get_text("autorenew_confirm_disable")
|
||||
confirm_text = (
|
||||
get_text("autorenew_confirm_enable") if enable else get_text("autorenew_confirm_disable")
|
||||
)
|
||||
kb = get_autorenew_confirm_keyboard(enable, sub.subscription_id, current_lang, i18n)
|
||||
try:
|
||||
await callback.message.edit_text(confirm_text, reply_markup=kb)
|
||||
@@ -1159,25 +1428,33 @@ async def confirm_autorenew_handler(
|
||||
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||
return
|
||||
if enable:
|
||||
has_saved_card = await user_billing_dal.user_has_saved_payment_method(session, callback.from_user.id)
|
||||
has_saved_card = await user_billing_dal.user_has_saved_payment_method(
|
||||
session, callback.from_user.id
|
||||
)
|
||||
if not has_saved_card:
|
||||
try:
|
||||
await callback.answer(get_text("autorenew_enable_requires_card"), show_alert=True)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
await my_subscription_command_handler(callback, i18n_data, settings, panel_service, subscription_service, session, bot)
|
||||
await my_subscription_command_handler(
|
||||
callback, i18n_data, settings, panel_service, subscription_service, session, bot
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
|
||||
await subscription_dal.update_subscription(session, sub.subscription_id, {"auto_renew_enabled": enable})
|
||||
await subscription_dal.update_subscription(
|
||||
session, sub.subscription_id, {"auto_renew_enabled": enable}
|
||||
)
|
||||
await session.commit()
|
||||
try:
|
||||
await callback.answer(get_text("subscription_autorenew_updated"))
|
||||
except Exception:
|
||||
pass
|
||||
await my_subscription_command_handler(callback, i18n_data, settings, panel_service, subscription_service, session, bot)
|
||||
await my_subscription_command_handler(
|
||||
callback, i18n_data, settings, panel_service, subscription_service, session, bot
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "autorenew:cancel")
|
||||
@@ -1196,6 +1473,7 @@ async def autorenew_cancel_from_webhook_button(
|
||||
|
||||
# Disable auto-renew on the active subscription
|
||||
from db.dal import subscription_dal
|
||||
|
||||
sub = await subscription_dal.get_active_subscription_by_user_id(session, callback.from_user.id)
|
||||
if not sub:
|
||||
try:
|
||||
@@ -1209,13 +1487,17 @@ async def autorenew_cancel_from_webhook_button(
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
await subscription_dal.update_subscription(session, sub.subscription_id, {"auto_renew_enabled": False})
|
||||
await subscription_dal.update_subscription(
|
||||
session, sub.subscription_id, {"auto_renew_enabled": False}
|
||||
)
|
||||
await session.commit()
|
||||
try:
|
||||
await callback.answer(get_text("subscription_autorenew_updated"))
|
||||
except Exception:
|
||||
pass
|
||||
await my_subscription_command_handler(callback, i18n_data, settings, panel_service, subscription_service, session, bot)
|
||||
await my_subscription_command_handler(
|
||||
callback, i18n_data, settings, panel_service, subscription_service, session, bot
|
||||
)
|
||||
|
||||
|
||||
@router.message(Command("connect"))
|
||||
@@ -1229,4 +1511,6 @@ async def connect_command_handler(
|
||||
bot: Bot,
|
||||
):
|
||||
logging.info(f"User {message.from_user.id} used /connect command.")
|
||||
await my_subscription_command_handler(message, i18n_data, settings, panel_service, subscription_service, session, bot)
|
||||
await my_subscription_command_handler(
|
||||
message, i18n_data, settings, panel_service, subscription_service, session, bot
|
||||
)
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
from aiogram import Router, F, types
|
||||
from typing import Optional, List
|
||||
from typing import List, Optional
|
||||
|
||||
from aiogram import F, Router, types
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
|
||||
from config.settings import Settings
|
||||
from bot.keyboards.inline.user_keyboards import (
|
||||
get_payment_methods_list_keyboard,
|
||||
get_bind_url_keyboard,
|
||||
get_payment_method_delete_confirm_keyboard,
|
||||
get_payment_method_details_keyboard,
|
||||
get_bind_url_keyboard,
|
||||
get_payment_methods_list_keyboard,
|
||||
)
|
||||
from bot.services.yookassa_service import YooKassaService
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
from bot.services.yookassa_service import YooKassaService
|
||||
from config.settings import Settings
|
||||
from db.dal import user_billing_dal
|
||||
from db.models import Payment
|
||||
from sqlalchemy.future import select
|
||||
|
||||
router = Router(name="user_subscription_payment_methods_router")
|
||||
|
||||
|
||||
@router.callback_query(F.data == "pm:manage")
|
||||
async def payment_methods_manage(callback: types.CallbackQuery, settings: Settings, i18n_data: dict, session: AsyncSession):
|
||||
async def payment_methods_manage(
|
||||
callback: types.CallbackQuery, settings: Settings, i18n_data: dict, session: AsyncSession
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not settings.yookassa_autopayments_active:
|
||||
@@ -32,6 +35,7 @@ async def payment_methods_manage(callback: types.CallbackQuery, settings: Settin
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key
|
||||
|
||||
from db.dal.user_billing_dal import list_user_payment_methods
|
||||
|
||||
get_text = _
|
||||
methods = await list_user_payment_methods(session, callback.from_user.id)
|
||||
cards: List[tuple] = []
|
||||
@@ -64,7 +68,9 @@ async def payment_methods_manage(callback: types.CallbackQuery, settings: Settin
|
||||
if not cards:
|
||||
text += "\n\n" + get_text("payment_method_none")
|
||||
|
||||
await callback.message.edit_text(text, reply_markup=get_payment_methods_list_keyboard(cards, 0, current_lang, i18n))
|
||||
await callback.message.edit_text(
|
||||
text, reply_markup=get_payment_methods_list_keyboard(cards, 0, current_lang, i18n)
|
||||
)
|
||||
try:
|
||||
await callback.answer()
|
||||
except Exception:
|
||||
@@ -72,7 +78,13 @@ async def payment_methods_manage(callback: types.CallbackQuery, settings: Settin
|
||||
|
||||
|
||||
@router.callback_query(F.data == "pm:bind")
|
||||
async def payment_method_bind(callback: types.CallbackQuery, settings: Settings, i18n_data: dict, session: AsyncSession, yookassa_service: YooKassaService):
|
||||
async def payment_method_bind(
|
||||
callback: types.CallbackQuery,
|
||||
settings: Settings,
|
||||
i18n_data: dict,
|
||||
session: AsyncSession,
|
||||
yookassa_service: YooKassaService,
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not settings.yookassa_autopayments_active:
|
||||
@@ -98,7 +110,10 @@ async def payment_method_bind(callback: types.CallbackQuery, settings: Settings,
|
||||
if not resp or not resp.get("confirmation_url"):
|
||||
await callback.answer(_("error_payment_gateway"), show_alert=True)
|
||||
return
|
||||
await callback.message.edit_text(_("payment_methods_title"), reply_markup=get_bind_url_keyboard(resp["confirmation_url"], current_lang, i18n))
|
||||
await callback.message.edit_text(
|
||||
_("payment_methods_title"),
|
||||
reply_markup=get_bind_url_keyboard(resp["confirmation_url"], current_lang, i18n),
|
||||
)
|
||||
try:
|
||||
await callback.answer()
|
||||
except Exception:
|
||||
@@ -106,7 +121,9 @@ async def payment_method_bind(callback: types.CallbackQuery, settings: Settings,
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("pm:delete_confirm"))
|
||||
async def payment_method_delete_confirm(callback: types.CallbackQuery, settings: Settings, i18n_data: dict):
|
||||
async def payment_method_delete_confirm(
|
||||
callback: types.CallbackQuery, settings: Settings, i18n_data: dict
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not settings.yookassa_autopayments_active:
|
||||
@@ -119,7 +136,10 @@ async def payment_method_delete_confirm(callback: types.CallbackQuery, settings:
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key
|
||||
parts = callback.data.split(":", 2)
|
||||
pm_id = parts[2] if len(parts) >= 3 else ""
|
||||
await callback.message.edit_text(_("payment_method_delete_confirm"), reply_markup=get_payment_method_delete_confirm_keyboard(pm_id, current_lang, i18n))
|
||||
await callback.message.edit_text(
|
||||
_("payment_method_delete_confirm"),
|
||||
reply_markup=get_payment_method_delete_confirm_keyboard(pm_id, current_lang, i18n),
|
||||
)
|
||||
try:
|
||||
await callback.answer()
|
||||
except Exception:
|
||||
@@ -127,7 +147,9 @@ async def payment_method_delete_confirm(callback: types.CallbackQuery, settings:
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("pm:delete"))
|
||||
async def payment_method_delete(callback: types.CallbackQuery, settings: Settings, i18n_data: dict, session: AsyncSession):
|
||||
async def payment_method_delete(
|
||||
callback: types.CallbackQuery, settings: Settings, i18n_data: dict, session: AsyncSession
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not settings.yookassa_autopayments_active:
|
||||
@@ -148,13 +170,20 @@ async def payment_method_delete(callback: types.CallbackQuery, settings: Setting
|
||||
delete_user_payment_method_by_provider_id,
|
||||
list_user_payment_methods,
|
||||
)
|
||||
|
||||
if pm_id_raw:
|
||||
if pm_id_raw.isdigit():
|
||||
deleted = await delete_user_payment_method(session, callback.from_user.id, int(pm_id_raw))
|
||||
deleted = await delete_user_payment_method(
|
||||
session, callback.from_user.id, int(pm_id_raw)
|
||||
)
|
||||
else:
|
||||
deleted = await delete_user_payment_method_by_provider_id(session, callback.from_user.id, pm_id_raw)
|
||||
deleted = await delete_user_payment_method_by_provider_id(
|
||||
session, callback.from_user.id, pm_id_raw
|
||||
)
|
||||
try:
|
||||
legacy_deleted = await user_billing_dal.delete_yk_payment_method(session, callback.from_user.id)
|
||||
legacy_deleted = await user_billing_dal.delete_yk_payment_method(
|
||||
session, callback.from_user.id
|
||||
)
|
||||
deleted = deleted or legacy_deleted
|
||||
except Exception:
|
||||
pass
|
||||
@@ -164,12 +193,15 @@ async def payment_method_delete(callback: types.CallbackQuery, settings: Setting
|
||||
text = _("payment_methods_title")
|
||||
cards = []
|
||||
for m in methods:
|
||||
|
||||
def _is_yoomoney_network(network: Optional[str]) -> bool:
|
||||
s = (network or "").lower()
|
||||
return "yoomoney" in s or "yoo money" in s or "yoo-money" in s
|
||||
|
||||
def _extract_last4(text: str) -> Optional[str]:
|
||||
digits = "".join(ch for ch in text if ch.isdigit())
|
||||
return digits[-4:] if len(digits) >= 4 else None
|
||||
|
||||
def _format_pm_title(network: Optional[str], last4: Optional[str]) -> str:
|
||||
if _is_yoomoney_network(network):
|
||||
l4 = last4 or _extract_last4(network or "")
|
||||
@@ -181,12 +213,16 @@ async def payment_method_delete(callback: types.CallbackQuery, settings: Setting
|
||||
return _("payment_method_card_title", network=network_name, last4=last4)
|
||||
network_name = network or _("payment_network_generic")
|
||||
return _("payment_method_generic_title", network=network_name)
|
||||
|
||||
title = _format_pm_title(m.card_network, m.card_last4)
|
||||
cards.append((str(m.method_id), title if not m.is_default else f"⭐ {title}"))
|
||||
if not cards:
|
||||
text += "\n\n" + _("payment_method_none")
|
||||
msg = _("payment_method_deleted_success") if deleted else _("error_try_again")
|
||||
await callback.message.edit_text(f"{msg}\n\n{text}", reply_markup=get_payment_methods_list_keyboard(cards, 0, current_lang, i18n))
|
||||
await callback.message.edit_text(
|
||||
f"{msg}\n\n{text}",
|
||||
reply_markup=get_payment_methods_list_keyboard(cards, 0, current_lang, i18n),
|
||||
)
|
||||
try:
|
||||
await callback.answer()
|
||||
except Exception:
|
||||
@@ -201,7 +237,9 @@ async def payment_method_delete(callback: types.CallbackQuery, settings: Setting
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("pm:view"))
|
||||
async def payment_method_view(callback: types.CallbackQuery, settings: Settings, i18n_data: dict, session: AsyncSession):
|
||||
async def payment_method_view(
|
||||
callback: types.CallbackQuery, settings: Settings, i18n_data: dict, session: AsyncSession
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not settings.yookassa_autopayments_active:
|
||||
@@ -216,13 +254,21 @@ async def payment_method_view(callback: types.CallbackQuery, settings: Settings,
|
||||
billing = await user_billing_dal.get_user_billing(session, callback.from_user.id)
|
||||
if not billing or not billing.yookassa_payment_method_id:
|
||||
from db.dal.user_billing_dal import list_user_payment_methods
|
||||
|
||||
methods = await list_user_payment_methods(session, callback.from_user.id)
|
||||
if not methods:
|
||||
await callback.answer(_("payment_method_none"), show_alert=True)
|
||||
return
|
||||
parts = callback.data.split(":", 2)
|
||||
pm_id = parts[2] if len(parts) >= 3 else str(methods[0].method_id)
|
||||
sel = next((m for m in methods if str(m.method_id) == pm_id or m.provider_payment_method_id == pm_id), methods[0])
|
||||
sel = next(
|
||||
(
|
||||
m
|
||||
for m in methods
|
||||
if str(m.method_id) == pm_id or m.provider_payment_method_id == pm_id
|
||||
),
|
||||
methods[0],
|
||||
)
|
||||
|
||||
def _is_yoomoney_network(network: Optional[str]) -> bool:
|
||||
s = (network or "").lower()
|
||||
@@ -245,15 +291,15 @@ async def payment_method_view(callback: types.CallbackQuery, settings: Settings,
|
||||
return _("payment_method_generic_title", network=network_name)
|
||||
|
||||
title = _format_pm_title(sel.card_network, sel.card_last4)
|
||||
added_at = sel.created_at.strftime('%Y-%m-%d') if getattr(sel, 'created_at', None) else "—"
|
||||
added_at = sel.created_at.strftime("%Y-%m-%d") if getattr(sel, "created_at", None) else "—"
|
||||
last_tx = "—"
|
||||
try:
|
||||
stmt = (
|
||||
select(Payment)
|
||||
.where(
|
||||
Payment.user_id == callback.from_user.id,
|
||||
Payment.status == 'succeeded',
|
||||
Payment.provider == 'yookassa',
|
||||
Payment.status == "succeeded",
|
||||
Payment.provider == "yookassa",
|
||||
)
|
||||
.order_by(Payment.created_at.desc())
|
||||
.limit(1)
|
||||
@@ -261,26 +307,33 @@ async def payment_method_view(callback: types.CallbackQuery, settings: Settings,
|
||||
result = await session.execute(stmt)
|
||||
lp = result.scalar_one_or_none()
|
||||
if lp and lp.created_at:
|
||||
last_tx = lp.created_at.strftime('%Y-%m-%d')
|
||||
last_tx = lp.created_at.strftime("%Y-%m-%d")
|
||||
except Exception:
|
||||
pass
|
||||
details = f"{title}\n{_('payment_method_added_at', date=added_at)}\n{_('payment_method_last_tx', date=last_tx)}"
|
||||
await callback.message.edit_text(details, reply_markup=get_payment_method_details_keyboard(str(sel.method_id), current_lang, i18n))
|
||||
await callback.message.edit_text(
|
||||
details,
|
||||
reply_markup=get_payment_method_details_keyboard(
|
||||
str(sel.method_id), current_lang, i18n
|
||||
),
|
||||
)
|
||||
try:
|
||||
await callback.answer()
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
|
||||
added_at = billing.created_at.strftime('%Y-%m-%d') if getattr(billing, 'created_at', None) else "—"
|
||||
added_at = (
|
||||
billing.created_at.strftime("%Y-%m-%d") if getattr(billing, "created_at", None) else "—"
|
||||
)
|
||||
last_tx = "—"
|
||||
try:
|
||||
stmt = (
|
||||
select(Payment)
|
||||
.where(
|
||||
Payment.user_id == callback.from_user.id,
|
||||
Payment.status == 'succeeded',
|
||||
Payment.provider == 'yookassa',
|
||||
Payment.status == "succeeded",
|
||||
Payment.provider == "yookassa",
|
||||
)
|
||||
.order_by(Payment.created_at.desc())
|
||||
.limit(1)
|
||||
@@ -288,7 +341,7 @@ async def payment_method_view(callback: types.CallbackQuery, settings: Settings,
|
||||
result = await session.execute(stmt)
|
||||
last_payment = result.scalar_one_or_none()
|
||||
if last_payment and last_payment.created_at:
|
||||
last_tx = last_payment.created_at.strftime('%Y-%m-%d')
|
||||
last_tx = last_payment.created_at.strftime("%Y-%m-%d")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -314,7 +367,12 @@ async def payment_method_view(callback: types.CallbackQuery, settings: Settings,
|
||||
|
||||
title = _format_pm_title(billing.card_network, billing.card_last4)
|
||||
details = f"{title}\n{_('payment_method_added_at', date=added_at)}\n{_('payment_method_last_tx', date=last_tx)}"
|
||||
await callback.message.edit_text(details, reply_markup=get_payment_method_details_keyboard(billing.yookassa_payment_method_id, current_lang, i18n))
|
||||
await callback.message.edit_text(
|
||||
details,
|
||||
reply_markup=get_payment_method_details_keyboard(
|
||||
billing.yookassa_payment_method_id, current_lang, i18n
|
||||
),
|
||||
)
|
||||
try:
|
||||
await callback.answer()
|
||||
except Exception:
|
||||
@@ -322,7 +380,13 @@ async def payment_method_view(callback: types.CallbackQuery, settings: Settings,
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("pm:history"))
|
||||
async def payment_method_history(callback: types.CallbackQuery, settings: Settings, i18n_data: dict, session: AsyncSession, yookassa_service: YooKassaService):
|
||||
async def payment_method_history(
|
||||
callback: types.CallbackQuery,
|
||||
settings: Settings,
|
||||
i18n_data: dict,
|
||||
session: AsyncSession,
|
||||
yookassa_service: YooKassaService,
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not settings.yookassa_autopayments_active:
|
||||
@@ -335,6 +399,7 @@ async def payment_method_history(callback: types.CallbackQuery, settings: Settin
|
||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key
|
||||
|
||||
from db.dal import payment_dal
|
||||
|
||||
payments = await payment_dal.get_recent_payment_logs_with_user(session, limit=30, offset=0)
|
||||
user_payments = [p for p in payments if p.user_id == callback.from_user.id]
|
||||
|
||||
@@ -346,6 +411,7 @@ async def payment_method_history(callback: types.CallbackQuery, settings: Settin
|
||||
pm_filter_requested = True
|
||||
if split_pm_id.isdigit():
|
||||
from db.dal.user_billing_dal import list_user_payment_methods
|
||||
|
||||
methods = await list_user_payment_methods(session, callback.from_user.id)
|
||||
sel = next((m for m in methods if str(m.method_id) == split_pm_id), None)
|
||||
if sel and sel.provider_payment_method_id:
|
||||
@@ -362,7 +428,7 @@ async def payment_method_history(callback: types.CallbackQuery, settings: Settin
|
||||
if selected_pm_provider_id:
|
||||
filtered: List[Payment] = []
|
||||
for p in user_payments:
|
||||
if p.provider != 'yookassa':
|
||||
if p.provider != "yookassa":
|
||||
continue
|
||||
if p.yookassa_payment_id and yookassa_service:
|
||||
try:
|
||||
@@ -376,7 +442,11 @@ async def payment_method_history(callback: types.CallbackQuery, settings: Settin
|
||||
user_payments = filtered
|
||||
|
||||
if not user_payments:
|
||||
from bot.keyboards.inline.user_keyboards import get_back_to_payment_method_details_keyboard, get_payment_methods_manage_keyboard
|
||||
from bot.keyboards.inline.user_keyboards import (
|
||||
get_back_to_payment_method_details_keyboard,
|
||||
get_payment_methods_manage_keyboard,
|
||||
)
|
||||
|
||||
back_pm_id = ""
|
||||
try:
|
||||
split_a, split_b, back_pm_id = callback.data.split(":", 2)
|
||||
@@ -395,11 +465,15 @@ async def payment_method_history(callback: types.CallbackQuery, settings: Settin
|
||||
def _format_item(p: Payment) -> str:
|
||||
if traffic_mode:
|
||||
units_val = p.subscription_duration_months or 0
|
||||
units_display = str(int(units_val)) if float(units_val).is_integer() else f"{units_val:g}"
|
||||
units_display = (
|
||||
str(int(units_val)) if float(units_val).is_integer() else f"{units_val:g}"
|
||||
)
|
||||
title = p.description or _("traffic_purchase_title", traffic_gb=units_display)
|
||||
else:
|
||||
title = p.description or _("subscription_purchase_title", months=p.subscription_duration_months or 1)
|
||||
date_str = p.created_at.strftime('%Y-%m-%d') if p.created_at else "N/A"
|
||||
title = p.description or _(
|
||||
"subscription_purchase_title", months=p.subscription_duration_months or 1
|
||||
)
|
||||
date_str = p.created_at.strftime("%Y-%m-%d") if p.created_at else "N/A"
|
||||
return f"{date_str} — {title} — {p.amount:.2f} {p.currency}"
|
||||
|
||||
lines = [_format_item(p) for p in user_payments]
|
||||
@@ -408,7 +482,11 @@ async def payment_method_history(callback: types.CallbackQuery, settings: Settin
|
||||
split_a, split_b, split_pm_id_for_back = callback.data.split(":", 2)
|
||||
except Exception:
|
||||
split_pm_id_for_back = ""
|
||||
from bot.keyboards.inline.user_keyboards import get_back_to_payment_method_details_keyboard, get_payment_methods_manage_keyboard
|
||||
from bot.keyboards.inline.user_keyboards import (
|
||||
get_back_to_payment_method_details_keyboard,
|
||||
get_payment_methods_manage_keyboard,
|
||||
)
|
||||
|
||||
back_markup = (
|
||||
get_back_to_payment_method_details_keyboard(split_pm_id_for_back, current_lang, i18n)
|
||||
if split_pm_id_for_back
|
||||
@@ -418,21 +496,27 @@ async def payment_method_history(callback: types.CallbackQuery, settings: Settin
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("pm:list:"))
|
||||
async def payment_methods_list(callback: types.CallbackQuery, settings: Settings, i18n_data: dict, session: AsyncSession):
|
||||
async def payment_methods_list(
|
||||
callback: types.CallbackQuery, settings: Settings, i18n_data: dict, session: AsyncSession
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
get_text = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key
|
||||
|
||||
from db.dal.user_billing_dal import list_user_payment_methods
|
||||
|
||||
cards: List[tuple] = []
|
||||
methods = await list_user_payment_methods(session, callback.from_user.id)
|
||||
for m in methods:
|
||||
|
||||
def _is_yoomoney_network(network: Optional[str]) -> bool:
|
||||
s = (network or "").lower()
|
||||
return "yoomoney" in s or "yoo money" in s or "yoo-money" in s
|
||||
|
||||
def _extract_last4(text: str) -> Optional[str]:
|
||||
digits = "".join(ch for ch in text if ch.isdigit())
|
||||
return digits[-4:] if len(digits) >= 4 else None
|
||||
|
||||
def _format_pm_title(network: Optional[str], last4: Optional[str]) -> str:
|
||||
if _is_yoomoney_network(network):
|
||||
l4 = last4 or _extract_last4(network or "")
|
||||
@@ -444,6 +528,7 @@ async def payment_methods_list(callback: types.CallbackQuery, settings: Settings
|
||||
return get_text("payment_method_card_title", network=network_name, last4=last4)
|
||||
network_name = network or get_text("payment_network_generic")
|
||||
return get_text("payment_method_generic_title", network=network_name)
|
||||
|
||||
title = _format_pm_title(m.card_network, m.card_last4)
|
||||
cards.append((str(m.method_id), title if not m.is_default else f"⭐ {title}"))
|
||||
|
||||
@@ -456,9 +541,10 @@ async def payment_methods_list(callback: types.CallbackQuery, settings: Settings
|
||||
text = get_text("payment_methods_title")
|
||||
if not cards:
|
||||
text += "\n\n" + get_text("payment_method_none")
|
||||
await callback.message.edit_text(text, reply_markup=get_payment_methods_list_keyboard(cards, page, current_lang, i18n))
|
||||
await callback.message.edit_text(
|
||||
text, reply_markup=get_payment_methods_list_keyboard(cards, page, current_lang, i18n)
|
||||
)
|
||||
try:
|
||||
await callback.answer()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Optional
|
||||
from typing import Optional
|
||||
|
||||
from aiogram import F, Router, types
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -21,7 +21,7 @@ async def pay_crypto_callback_handler(
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
get_text = (lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key)
|
||||
get_text = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key
|
||||
|
||||
if not i18n or not callback.message:
|
||||
try:
|
||||
@@ -30,7 +30,11 @@ async def pay_crypto_callback_handler(
|
||||
pass
|
||||
return
|
||||
|
||||
if not settings.CRYPTOPAY_ENABLED or not cryptopay_service or not getattr(cryptopay_service, "configured", False):
|
||||
if (
|
||||
not settings.CRYPTOPAY_ENABLED
|
||||
or not cryptopay_service
|
||||
or not getattr(cryptopay_service, "configured", False)
|
||||
):
|
||||
try:
|
||||
await callback.answer(get_text("payment_service_unavailable_alert"), show_alert=True)
|
||||
except Exception:
|
||||
@@ -56,7 +60,11 @@ async def pay_crypto_callback_handler(
|
||||
payment_description = (
|
||||
get_text("payment_description_traffic", traffic_gb=human_value)
|
||||
if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"}
|
||||
else (get_text("payment_description_hwid_devices", count=int(months)) if sale_base in {"hwid_device", "hwid_devices"} else get_text("payment_description_subscription", months=int(months)))
|
||||
else (
|
||||
get_text("payment_description_hwid_devices", count=int(months))
|
||||
if sale_base in {"hwid_device", "hwid_devices"}
|
||||
else get_text("payment_description_subscription", months=int(months))
|
||||
)
|
||||
)
|
||||
|
||||
invoice_url = await cryptopay_service.create_invoice(
|
||||
@@ -72,7 +80,9 @@ async def pay_crypto_callback_handler(
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
get_text(
|
||||
key="payment_link_message_traffic" if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"} else "payment_link_message",
|
||||
key="payment_link_message_traffic"
|
||||
if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"}
|
||||
else "payment_link_message",
|
||||
months=int(months),
|
||||
traffic_gb=human_value,
|
||||
),
|
||||
@@ -89,7 +99,9 @@ async def pay_crypto_callback_handler(
|
||||
try:
|
||||
await callback.message.answer(
|
||||
get_text(
|
||||
key="payment_link_message_traffic" if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"} else "payment_link_message",
|
||||
key="payment_link_message_traffic"
|
||||
if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"}
|
||||
else "payment_link_message",
|
||||
months=int(months),
|
||||
traffic_gb=human_value,
|
||||
),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import logging
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
@@ -65,9 +65,17 @@ async def pay_fk_callback_handler(
|
||||
payment_description = (
|
||||
get_text("payment_description_traffic", traffic_gb=human_value)
|
||||
if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"}
|
||||
else (get_text("payment_description_hwid_devices", count=int(months)) if sale_base in {"hwid_device", "hwid_devices"} else get_text("payment_description_subscription", months=int(months)))
|
||||
else (
|
||||
get_text("payment_description_hwid_devices", count=int(months))
|
||||
if sale_base in {"hwid_device", "hwid_devices"}
|
||||
else get_text("payment_description_subscription", months=int(months))
|
||||
)
|
||||
)
|
||||
currency_code = (
|
||||
getattr(freekassa_service, "default_currency", None)
|
||||
or settings.DEFAULT_CURRENCY_SYMBOL
|
||||
or "RUB"
|
||||
)
|
||||
currency_code = getattr(freekassa_service, "default_currency", None) or settings.DEFAULT_CURRENCY_SYMBOL or "RUB"
|
||||
|
||||
payment_record_payload = {
|
||||
"user_id": user_id,
|
||||
@@ -79,8 +87,12 @@ async def pay_fk_callback_handler(
|
||||
"provider": "freekassa",
|
||||
"sale_mode": sale_mode,
|
||||
"tariff_key": sale_mode.split("@", 1)[1] if "@" in sale_mode else None,
|
||||
"purchased_gb": float(months) if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"} else None,
|
||||
"purchased_hwid_devices": int(months) if sale_base in {"hwid_device", "hwid_devices"} else None,
|
||||
"purchased_gb": float(months)
|
||||
if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"}
|
||||
else None,
|
||||
"purchased_hwid_devices": int(months)
|
||||
if sale_base in {"hwid_device", "hwid_devices"}
|
||||
else None,
|
||||
}
|
||||
|
||||
try:
|
||||
@@ -138,7 +150,9 @@ async def pay_fk_callback_handler(
|
||||
)
|
||||
|
||||
if location:
|
||||
order_identifier_display = str(order_id_api or provider_identifier or payment_record.payment_id)
|
||||
order_identifier_display = str(
|
||||
order_id_api or provider_identifier or payment_record.payment_id
|
||||
)
|
||||
order_info_text = get_text(
|
||||
"free_kassa_order_info",
|
||||
order_id=order_identifier_display,
|
||||
@@ -146,8 +160,11 @@ async def pay_fk_callback_handler(
|
||||
)
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
f"{order_info_text}\n\n" + get_text(
|
||||
key="payment_link_message_traffic" if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"} else "payment_link_message",
|
||||
f"{order_info_text}\n\n"
|
||||
+ get_text(
|
||||
key="payment_link_message_traffic"
|
||||
if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"}
|
||||
else "payment_link_message",
|
||||
months=int(months),
|
||||
traffic_gb=human_value,
|
||||
),
|
||||
@@ -161,11 +178,16 @@ async def pay_fk_callback_handler(
|
||||
disable_web_page_preview=False,
|
||||
)
|
||||
except Exception as e_edit:
|
||||
logging.warning(f"FreeKassa: failed to display payment link ({e_edit}), sending new message.")
|
||||
logging.warning(
|
||||
f"FreeKassa: failed to display payment link ({e_edit}), sending new message."
|
||||
)
|
||||
try:
|
||||
await callback.message.answer(
|
||||
f"{order_info_text}\n\n" + get_text(
|
||||
key="payment_link_message_traffic" if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"} else "payment_link_message",
|
||||
f"{order_info_text}\n\n"
|
||||
+ get_text(
|
||||
key="payment_link_message_traffic"
|
||||
if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"}
|
||||
else "payment_link_message",
|
||||
months=int(months),
|
||||
traffic_gb=human_value,
|
||||
),
|
||||
@@ -207,7 +229,10 @@ async def pay_fk_callback_handler(
|
||||
await session.commit()
|
||||
except Exception as e_status:
|
||||
await session.rollback()
|
||||
logging.error(f"FreeKassa: failed to mark payment {payment_record.payment_id} as failed_creation: {e_status}", exc_info=True)
|
||||
logging.error(
|
||||
f"FreeKassa: failed to mark payment {payment_record.payment_id} as failed_creation: {e_status}",
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
try:
|
||||
await callback.message.edit_text(get_text("error_payment_gateway"))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import json
|
||||
import json
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
@@ -92,7 +92,11 @@ async def pay_platega_callback_handler(
|
||||
payment_description = (
|
||||
get_text("payment_description_traffic", traffic_gb=human_value)
|
||||
if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"}
|
||||
else (get_text("payment_description_hwid_devices", count=int(months)) if sale_base in {"hwid_device", "hwid_devices"} else get_text("payment_description_subscription", months=int(months)))
|
||||
else (
|
||||
get_text("payment_description_hwid_devices", count=int(months))
|
||||
if sale_base in {"hwid_device", "hwid_devices"}
|
||||
else get_text("payment_description_subscription", months=int(months))
|
||||
)
|
||||
)
|
||||
currency_code = settings.DEFAULT_CURRENCY_SYMBOL or "RUB"
|
||||
|
||||
@@ -106,8 +110,12 @@ async def pay_platega_callback_handler(
|
||||
"provider": "platega",
|
||||
"sale_mode": sale_mode,
|
||||
"tariff_key": sale_mode.split("@", 1)[1] if "@" in sale_mode else None,
|
||||
"purchased_gb": float(months) if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"} else None,
|
||||
"purchased_hwid_devices": int(months) if sale_base in {"hwid_device", "hwid_devices"} else None,
|
||||
"purchased_gb": float(months)
|
||||
if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"}
|
||||
else None,
|
||||
"purchased_hwid_devices": int(months)
|
||||
if sale_base in {"hwid_device", "hwid_devices"}
|
||||
else None,
|
||||
}
|
||||
|
||||
try:
|
||||
@@ -178,7 +186,9 @@ async def pay_platega_callback_handler(
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
get_text(
|
||||
key="payment_link_message_traffic" if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"} else "payment_link_message",
|
||||
key="payment_link_message_traffic"
|
||||
if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"}
|
||||
else "payment_link_message",
|
||||
months=int(months),
|
||||
traffic_gb=human_value,
|
||||
),
|
||||
@@ -192,11 +202,15 @@ async def pay_platega_callback_handler(
|
||||
disable_web_page_preview=False,
|
||||
)
|
||||
except Exception as e_edit:
|
||||
logging.warning(f"Platega: failed to display payment link ({e_edit}), sending new message.")
|
||||
logging.warning(
|
||||
f"Platega: failed to display payment link ({e_edit}), sending new message."
|
||||
)
|
||||
try:
|
||||
await callback.message.answer(
|
||||
get_text(
|
||||
key="payment_link_message_traffic" if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"} else "payment_link_message",
|
||||
key="payment_link_message_traffic"
|
||||
if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"}
|
||||
else "payment_link_message",
|
||||
months=int(months),
|
||||
traffic_gb=human_value,
|
||||
),
|
||||
@@ -232,7 +246,10 @@ async def pay_platega_callback_handler(
|
||||
await session.commit()
|
||||
except Exception as e_status:
|
||||
await session.rollback()
|
||||
logging.error(f"Platega: failed to mark payment {payment_record.payment_id} as failed_creation: {e_status}", exc_info=True)
|
||||
logging.error(
|
||||
f"Platega: failed to mark payment {payment_record.payment_id} as failed_creation: {e_status}",
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
try:
|
||||
await callback.message.edit_text(get_text("error_payment_gateway"))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import logging
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from aiogram import F, Router, types
|
||||
@@ -64,7 +64,11 @@ async def pay_severpay_callback_handler(
|
||||
payment_description = (
|
||||
get_text("payment_description_traffic", traffic_gb=human_value)
|
||||
if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"}
|
||||
else (get_text("payment_description_hwid_devices", count=int(months)) if sale_base in {"hwid_device", "hwid_devices"} else get_text("payment_description_subscription", months=int(months)))
|
||||
else (
|
||||
get_text("payment_description_hwid_devices", count=int(months))
|
||||
if sale_base in {"hwid_device", "hwid_devices"}
|
||||
else get_text("payment_description_subscription", months=int(months))
|
||||
)
|
||||
)
|
||||
currency_code = settings.DEFAULT_CURRENCY_SYMBOL or "RUB"
|
||||
|
||||
@@ -78,8 +82,12 @@ async def pay_severpay_callback_handler(
|
||||
"provider": "severpay",
|
||||
"sale_mode": sale_mode,
|
||||
"tariff_key": sale_mode.split("@", 1)[1] if "@" in sale_mode else None,
|
||||
"purchased_gb": float(months) if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"} else None,
|
||||
"purchased_hwid_devices": int(months) if sale_base in {"hwid_device", "hwid_devices"} else None,
|
||||
"purchased_gb": float(months)
|
||||
if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"}
|
||||
else None,
|
||||
"purchased_hwid_devices": int(months)
|
||||
if sale_base in {"hwid_device", "hwid_devices"}
|
||||
else None,
|
||||
}
|
||||
|
||||
try:
|
||||
@@ -138,7 +146,9 @@ async def pay_severpay_callback_handler(
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
get_text(
|
||||
key="payment_link_message_traffic" if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"} else "payment_link_message",
|
||||
key="payment_link_message_traffic"
|
||||
if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"}
|
||||
else "payment_link_message",
|
||||
months=int(months),
|
||||
traffic_gb=human_value,
|
||||
),
|
||||
@@ -152,11 +162,15 @@ async def pay_severpay_callback_handler(
|
||||
disable_web_page_preview=False,
|
||||
)
|
||||
except Exception as e_edit:
|
||||
logging.warning(f"SeverPay: failed to display payment link ({e_edit}), sending new message.")
|
||||
logging.warning(
|
||||
f"SeverPay: failed to display payment link ({e_edit}), sending new message."
|
||||
)
|
||||
try:
|
||||
await callback.message.answer(
|
||||
get_text(
|
||||
key="payment_link_message_traffic" if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"} else "payment_link_message",
|
||||
key="payment_link_message_traffic"
|
||||
if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"}
|
||||
else "payment_link_message",
|
||||
months=int(months),
|
||||
traffic_gb=human_value,
|
||||
),
|
||||
@@ -192,7 +206,10 @@ async def pay_severpay_callback_handler(
|
||||
await session.commit()
|
||||
except Exception as e_status:
|
||||
await session.rollback()
|
||||
logging.error(f"SeverPay: failed to mark payment {payment_record.payment_id} as failed_creation: {e_status}", exc_info=True)
|
||||
logging.error(
|
||||
f"SeverPay: failed to mark payment {payment_record.payment_id} as failed_creation: {e_status}",
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
try:
|
||||
await callback.message.edit_text(get_text("error_payment_gateway"))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import logging
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from aiogram import F, Router, types
|
||||
@@ -22,7 +22,7 @@ async def pay_stars_callback_handler(
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
get_text = (lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key)
|
||||
get_text = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key
|
||||
|
||||
if not i18n or not callback.message:
|
||||
try:
|
||||
@@ -57,7 +57,11 @@ async def pay_stars_callback_handler(
|
||||
payment_description = (
|
||||
get_text("payment_description_traffic", traffic_gb=human_value)
|
||||
if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"}
|
||||
else (get_text("payment_description_hwid_devices", count=int(months)) if sale_base in {"hwid_device", "hwid_devices"} else get_text("payment_description_subscription", months=int(months)))
|
||||
else (
|
||||
get_text("payment_description_hwid_devices", count=int(months))
|
||||
if sale_base in {"hwid_device", "hwid_devices"}
|
||||
else get_text("payment_description_subscription", months=int(months))
|
||||
)
|
||||
)
|
||||
|
||||
payment_db_id = await stars_service.create_invoice(
|
||||
@@ -73,16 +77,22 @@ async def pay_stars_callback_handler(
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
get_text(
|
||||
"payment_invoice_sent_message_traffic" if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"} else "payment_invoice_sent_message",
|
||||
"payment_invoice_sent_message_traffic"
|
||||
if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"}
|
||||
else "payment_invoice_sent_message",
|
||||
months=int(months),
|
||||
traffic_gb=human_value,
|
||||
),
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(
|
||||
text=get_text("back_to_payment_methods_button"),
|
||||
callback_data=f"subscribe_period:{human_value}",
|
||||
)]
|
||||
]),
|
||||
reply_markup=InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=get_text("back_to_payment_methods_button"),
|
||||
callback_data=f"subscribe_period:{human_value}",
|
||||
)
|
||||
]
|
||||
]
|
||||
),
|
||||
)
|
||||
except Exception as e_edit:
|
||||
logging.warning(f"Stars payment: failed to show invoice info message ({e_edit})")
|
||||
@@ -115,8 +125,9 @@ async def handle_successful_stars_payment(
|
||||
session: AsyncSession,
|
||||
stars_service: StarsService,
|
||||
):
|
||||
payload = (message.successful_payment.invoice_payload
|
||||
if message and message.successful_payment else "")
|
||||
payload = (
|
||||
message.successful_payment.invoice_payload if message and message.successful_payment else ""
|
||||
)
|
||||
try:
|
||||
parts = (payload or "").split(":")
|
||||
payment_db_id = int(parts[0])
|
||||
|
||||
@@ -43,7 +43,9 @@ async def select_subscription_period_callback_handler(
|
||||
return
|
||||
|
||||
price_source = traffic_packages if traffic_mode else settings.subscription_options
|
||||
stars_price_source = stars_traffic_packages if traffic_mode else settings.stars_subscription_options
|
||||
stars_price_source = (
|
||||
stars_traffic_packages if traffic_mode else settings.stars_subscription_options
|
||||
)
|
||||
|
||||
price_rub = price_source.get(months)
|
||||
stars_price = stars_price_source.get(months)
|
||||
@@ -82,7 +84,11 @@ async def select_subscription_period_callback_handler(
|
||||
pass
|
||||
return
|
||||
|
||||
text_content = get_text("choose_payment_method_traffic") if traffic_mode else get_text("choose_payment_method")
|
||||
text_content = (
|
||||
get_text("choose_payment_method_traffic")
|
||||
if traffic_mode
|
||||
else get_text("choose_payment_method")
|
||||
)
|
||||
reply_markup = get_payment_method_keyboard(
|
||||
months,
|
||||
price_rub,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import logging
|
||||
import logging
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from aiogram import F, Router, types
|
||||
@@ -37,7 +37,9 @@ def _sale_mode_base(sale_mode: str) -> str:
|
||||
return (sale_mode or "subscription").split("@", 1)[0].split("|", 1)[0]
|
||||
|
||||
|
||||
def _format_saved_payment_method_title(get_text, network: Optional[str], last4: Optional[str], is_default: bool) -> str:
|
||||
def _format_saved_payment_method_title(
|
||||
get_text, network: Optional[str], last4: Optional[str], is_default: bool
|
||||
) -> str:
|
||||
def _is_yoomoney_network(name: Optional[str]) -> bool:
|
||||
s = (name or "").lower()
|
||||
return "yoomoney" in s or "yoo money" in s or "yoo-money" in s
|
||||
@@ -85,7 +87,11 @@ async def _initiate_yk_payment(
|
||||
payment_description = (
|
||||
get_text("payment_description_traffic", traffic_gb=_format_value(months))
|
||||
if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"}
|
||||
else (get_text("payment_description_hwid_devices", count=int(months)) if sale_base in {"hwid_device", "hwid_devices"} else get_text("payment_description_subscription", months=int(months)))
|
||||
else (
|
||||
get_text("payment_description_hwid_devices", count=int(months))
|
||||
if sale_base in {"hwid_device", "hwid_devices"}
|
||||
else get_text("payment_description_subscription", months=int(months))
|
||||
)
|
||||
)
|
||||
payment_record_data = {
|
||||
"user_id": user_id,
|
||||
@@ -96,8 +102,12 @@ async def _initiate_yk_payment(
|
||||
"subscription_duration_months": int(months) if sale_base == "subscription" else None,
|
||||
"sale_mode": sale_base,
|
||||
"tariff_key": sale_mode.split("@", 1)[1] if "@" in sale_mode else None,
|
||||
"purchased_gb": float(months) if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"} else None,
|
||||
"purchased_hwid_devices": int(months) if sale_base in {"hwid_device", "hwid_devices"} else None,
|
||||
"purchased_gb": float(months)
|
||||
if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"}
|
||||
else None,
|
||||
"purchased_hwid_devices": int(months)
|
||||
if sale_base in {"hwid_device", "hwid_devices"}
|
||||
else None,
|
||||
}
|
||||
|
||||
db_payment_record = None
|
||||
@@ -157,7 +167,11 @@ async def _initiate_yk_payment(
|
||||
title = pm.get("title")
|
||||
card = pm.get("card") or {}
|
||||
account_number = pm.get("account_number") or pm.get("account")
|
||||
if isinstance(card, dict) and (pm_type or "").lower() in {"bank_card", "bank-card", "card"}:
|
||||
if isinstance(card, dict) and (pm_type or "").lower() in {
|
||||
"bank_card",
|
||||
"bank-card",
|
||||
"card",
|
||||
}:
|
||||
display_network = card.get("card_type") or title or "Card"
|
||||
display_last4 = card.get("last4")
|
||||
elif (pm_type or "").lower() in {"yoo_money", "yoomoney", "yoo-money", "wallet"}:
|
||||
@@ -206,7 +220,9 @@ async def _initiate_yk_payment(
|
||||
session, user_id, selected_method_internal_id
|
||||
)
|
||||
except Exception:
|
||||
logging.exception("Failed to set default payment method after initiating payment")
|
||||
logging.exception(
|
||||
"Failed to set default payment method after initiating payment"
|
||||
)
|
||||
await session.commit()
|
||||
except Exception as e_db_update_ykid:
|
||||
await session.rollback()
|
||||
@@ -223,7 +239,9 @@ async def _initiate_yk_payment(
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
get_text(
|
||||
key="payment_link_message_traffic" if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"} else "payment_link_message",
|
||||
key="payment_link_message_traffic"
|
||||
if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"}
|
||||
else "payment_link_message",
|
||||
months=int(months),
|
||||
traffic_gb=_format_value(months),
|
||||
),
|
||||
@@ -237,13 +255,13 @@ async def _initiate_yk_payment(
|
||||
disable_web_page_preview=False,
|
||||
)
|
||||
except Exception as e_edit:
|
||||
logging.warning(
|
||||
f"Edit message for payment link failed: {e_edit}. Sending new one."
|
||||
)
|
||||
logging.warning(f"Edit message for payment link failed: {e_edit}. Sending new one.")
|
||||
try:
|
||||
await callback.message.answer(
|
||||
get_text(
|
||||
key="payment_link_message_traffic" if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"} else "payment_link_message",
|
||||
key="payment_link_message_traffic"
|
||||
if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"}
|
||||
else "payment_link_message",
|
||||
months=int(months),
|
||||
traffic_gb=_format_value(months),
|
||||
),
|
||||
@@ -275,7 +293,9 @@ async def _initiate_yk_payment(
|
||||
session, user_id, selected_method_internal_id
|
||||
)
|
||||
except Exception:
|
||||
logging.exception("Failed to set default payment method after saved-card payment start")
|
||||
logging.exception(
|
||||
"Failed to set default payment method after saved-card payment start"
|
||||
)
|
||||
await session.commit()
|
||||
except Exception as e_db_update_saved:
|
||||
await session.rollback()
|
||||
@@ -328,7 +348,13 @@ async def _initiate_yk_payment(
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("pay_yk:"))
|
||||
async def pay_yk_callback_handler(callback: types.CallbackQuery, settings: Settings, i18n_data: dict, yookassa_service: YooKassaService, session: AsyncSession):
|
||||
async def pay_yk_callback_handler(
|
||||
callback: types.CallbackQuery,
|
||||
settings: Settings,
|
||||
i18n_data: dict,
|
||||
yookassa_service: YooKassaService,
|
||||
session: AsyncSession,
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
get_text = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key
|
||||
@@ -372,9 +398,13 @@ async def pay_yk_callback_handler(callback: types.CallbackQuery, settings: Setti
|
||||
months, price_rub, sale_mode = parsed
|
||||
user_id = callback.from_user.id
|
||||
currency_code_for_yk = "RUB"
|
||||
autopay_enabled = bool(settings.yookassa_autopayments_active and _sale_mode_base(sale_mode) == "subscription" and not settings.traffic_sale_mode)
|
||||
autopay_enabled = bool(
|
||||
settings.yookassa_autopayments_active
|
||||
and _sale_mode_base(sale_mode) == "subscription"
|
||||
and not settings.traffic_sale_mode
|
||||
)
|
||||
autopay_require_binding = bool(
|
||||
getattr(settings, 'YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING', True)
|
||||
getattr(settings, "YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING", True)
|
||||
)
|
||||
saved_methods: List = []
|
||||
if autopay_enabled:
|
||||
@@ -444,7 +474,13 @@ async def pay_yk_callback_handler(callback: types.CallbackQuery, settings: Setti
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("pay_yk_new:"))
|
||||
async def pay_yk_new_card_handler(callback: types.CallbackQuery, settings: Settings, i18n_data: dict, yookassa_service: YooKassaService, session: AsyncSession):
|
||||
async def pay_yk_new_card_handler(
|
||||
callback: types.CallbackQuery,
|
||||
settings: Settings,
|
||||
i18n_data: dict,
|
||||
yookassa_service: YooKassaService,
|
||||
session: AsyncSession,
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
get_text = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key
|
||||
@@ -490,9 +526,13 @@ async def pay_yk_new_card_handler(callback: types.CallbackQuery, settings: Setti
|
||||
months, price_rub, sale_mode = parsed
|
||||
user_id = callback.from_user.id
|
||||
currency_code_for_yk = "RUB"
|
||||
autopay_enabled = bool(settings.yookassa_autopayments_active and _sale_mode_base(sale_mode) == "subscription" and not settings.traffic_sale_mode)
|
||||
autopay_enabled = bool(
|
||||
settings.yookassa_autopayments_active
|
||||
and _sale_mode_base(sale_mode) == "subscription"
|
||||
and not settings.traffic_sale_mode
|
||||
)
|
||||
autopay_require_binding = bool(
|
||||
getattr(settings, 'YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING', True)
|
||||
getattr(settings, "YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING", True)
|
||||
)
|
||||
|
||||
await _initiate_yk_payment(
|
||||
@@ -518,7 +558,13 @@ async def pay_yk_new_card_handler(callback: types.CallbackQuery, settings: Setti
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("pay_yk_saved_list:"))
|
||||
async def pay_yk_saved_list_handler(callback: types.CallbackQuery, settings: Settings, i18n_data: dict, yookassa_service: YooKassaService, session: AsyncSession):
|
||||
async def pay_yk_saved_list_handler(
|
||||
callback: types.CallbackQuery,
|
||||
settings: Settings,
|
||||
i18n_data: dict,
|
||||
yookassa_service: YooKassaService,
|
||||
session: AsyncSession,
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
get_text = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key
|
||||
@@ -562,7 +608,11 @@ async def pay_yk_saved_list_handler(callback: types.CallbackQuery, settings: Set
|
||||
pass
|
||||
return
|
||||
|
||||
autopay_enabled = bool(settings.yookassa_autopayments_active and _sale_mode_base(sale_mode) == "subscription" and not settings.traffic_sale_mode)
|
||||
autopay_enabled = bool(
|
||||
settings.yookassa_autopayments_active
|
||||
and _sale_mode_base(sale_mode) == "subscription"
|
||||
and not settings.traffic_sale_mode
|
||||
)
|
||||
if not autopay_enabled:
|
||||
try:
|
||||
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||
@@ -662,7 +712,13 @@ async def pay_yk_saved_list_handler(callback: types.CallbackQuery, settings: Set
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("pay_yk_use_saved:"))
|
||||
async def pay_yk_use_saved_handler(callback: types.CallbackQuery, settings: Settings, i18n_data: dict, yookassa_service: YooKassaService, session: AsyncSession):
|
||||
async def pay_yk_use_saved_handler(
|
||||
callback: types.CallbackQuery,
|
||||
settings: Settings,
|
||||
i18n_data: dict,
|
||||
yookassa_service: YooKassaService,
|
||||
session: AsyncSession,
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
get_text = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key
|
||||
@@ -717,7 +773,11 @@ async def pay_yk_use_saved_handler(callback: types.CallbackQuery, settings: Sett
|
||||
pass
|
||||
return
|
||||
|
||||
autopay_enabled = bool(settings.yookassa_autopayments_active and _sale_mode_base(sale_mode) == "subscription" and not settings.traffic_sale_mode)
|
||||
autopay_enabled = bool(
|
||||
settings.yookassa_autopayments_active
|
||||
and _sale_mode_base(sale_mode) == "subscription"
|
||||
and not settings.traffic_sale_mode
|
||||
)
|
||||
if not autopay_enabled:
|
||||
try:
|
||||
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||
@@ -747,7 +807,9 @@ async def pay_yk_use_saved_handler(callback: types.CallbackQuery, settings: Sett
|
||||
break
|
||||
|
||||
if not selected_method:
|
||||
logging.warning(f"Selected payment method not found for user {user_id}: {method_identifier}")
|
||||
logging.warning(
|
||||
f"Selected payment method not found for user {user_id}: {method_identifier}"
|
||||
)
|
||||
try:
|
||||
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||
except Exception:
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import logging
|
||||
from aiogram import Router, F, types, Bot
|
||||
from typing import Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from aiogram import F, Router, types
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
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 NotificationService
|
||||
from bot.keyboards.inline.user_keyboards import (
|
||||
get_trial_confirmation_keyboard,
|
||||
get_main_menu_inline_keyboard,
|
||||
get_connect_and_main_keyboard,
|
||||
get_main_menu_inline_keyboard,
|
||||
)
|
||||
from bot.utils.config_link import prepare_config_links
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
from bot.services.notification_service import NotificationService
|
||||
from bot.services.panel_api_service import PanelApiService
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
from bot.utils.config_link import prepare_config_links
|
||||
from config.settings import Settings
|
||||
|
||||
from .start import send_main_menu
|
||||
|
||||
router = Router(name="user_trial_router")
|
||||
@@ -47,9 +48,7 @@ async def request_trial_confirmation_handler(
|
||||
if not settings.TRIAL_ENABLED:
|
||||
await callback.message.edit_text(
|
||||
_("trial_feature_disabled"),
|
||||
reply_markup=get_main_menu_inline_keyboard(
|
||||
current_lang, i18n, settings, False
|
||||
),
|
||||
reply_markup=get_main_menu_inline_keyboard(current_lang, i18n, settings, False),
|
||||
)
|
||||
try:
|
||||
await callback.answer()
|
||||
@@ -60,9 +59,7 @@ async def request_trial_confirmation_handler(
|
||||
if await subscription_service.has_had_any_subscription(session, user_id):
|
||||
await callback.message.edit_text(
|
||||
_("trial_already_had_subscription_or_trial"),
|
||||
reply_markup=get_main_menu_inline_keyboard(
|
||||
current_lang, i18n, settings, False
|
||||
),
|
||||
reply_markup=get_main_menu_inline_keyboard(current_lang, i18n, settings, False),
|
||||
)
|
||||
try:
|
||||
await callback.answer()
|
||||
@@ -71,9 +68,7 @@ async def request_trial_confirmation_handler(
|
||||
return
|
||||
|
||||
# Directly activate trial without confirmation
|
||||
activation_result = await subscription_service.activate_trial_subscription(
|
||||
session, user_id
|
||||
)
|
||||
activation_result = await subscription_service.activate_trial_subscription(session, user_id)
|
||||
|
||||
final_message_text_in_chat = ""
|
||||
show_trial_button_after_action = False
|
||||
@@ -93,9 +88,7 @@ async def request_trial_confirmation_handler(
|
||||
)
|
||||
config_link_for_trial = config_link_display_for_trial or _("config_link_not_available")
|
||||
|
||||
traffic_gb_val = activation_result.get(
|
||||
"traffic_gb", settings.TRIAL_TRAFFIC_LIMIT_GB
|
||||
)
|
||||
traffic_gb_val = activation_result.get("traffic_gb", settings.TRIAL_TRAFFIC_LIMIT_GB)
|
||||
traffic_display = (
|
||||
f"{traffic_gb_val} GB"
|
||||
if traffic_gb_val and traffic_gb_val > 0
|
||||
@@ -106,20 +99,19 @@ async def request_trial_confirmation_handler(
|
||||
"trial_activated_details_message",
|
||||
days=activation_result.get("days", settings.TRIAL_DURATION_DAYS),
|
||||
end_date=(
|
||||
end_date_obj.strftime("%Y-%m-%d")
|
||||
if isinstance(end_date_obj, datetime)
|
||||
else "N/A"
|
||||
end_date_obj.strftime("%Y-%m-%d") if isinstance(end_date_obj, datetime) else "N/A"
|
||||
),
|
||||
config_link=config_link_for_trial,
|
||||
traffic_gb=traffic_display,
|
||||
)
|
||||
|
||||
|
||||
# Send notification to admin about new trial
|
||||
notification_service = NotificationService(callback.bot, settings, i18n)
|
||||
await notification_service.notify_trial_activation(user_id, end_date_obj)
|
||||
# Mark ad attribution trial if exists
|
||||
try:
|
||||
from db.dal import ad_dal as _ad_dal
|
||||
|
||||
await _ad_dal.mark_trial_activated(session, user_id)
|
||||
await session.commit()
|
||||
except Exception as e_mark:
|
||||
@@ -136,11 +128,8 @@ async def request_trial_confirmation_handler(
|
||||
await callback.answer(final_message_text_in_chat, show_alert=True)
|
||||
except Exception:
|
||||
pass
|
||||
if (
|
||||
settings.TRIAL_ENABLED
|
||||
and not await subscription_service.has_had_any_subscription(
|
||||
session, user_id
|
||||
)
|
||||
if settings.TRIAL_ENABLED and not await subscription_service.has_had_any_subscription(
|
||||
session, user_id
|
||||
):
|
||||
show_trial_button_after_action = True
|
||||
|
||||
@@ -166,9 +155,7 @@ async def request_trial_confirmation_handler(
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
except Exception as e_edit:
|
||||
logging.warning(
|
||||
f"Could not edit trial result message: {e_edit}. Sending new one."
|
||||
)
|
||||
logging.warning(f"Could not edit trial result message: {e_edit}. Sending new one.")
|
||||
|
||||
if callback.message:
|
||||
await callback.message.answer(
|
||||
@@ -213,9 +200,7 @@ async def confirm_activate_trial_handler(
|
||||
return
|
||||
if await subscription_service.has_had_any_subscription(session, user_id):
|
||||
try:
|
||||
await callback.answer(
|
||||
_("trial_already_had_subscription_or_trial"), show_alert=True
|
||||
)
|
||||
await callback.answer(_("trial_already_had_subscription_or_trial"), show_alert=True)
|
||||
except Exception:
|
||||
pass
|
||||
await send_main_menu(
|
||||
@@ -223,9 +208,7 @@ async def confirm_activate_trial_handler(
|
||||
)
|
||||
return
|
||||
|
||||
activation_result = await subscription_service.activate_trial_subscription(
|
||||
session, user_id
|
||||
)
|
||||
activation_result = await subscription_service.activate_trial_subscription(session, user_id)
|
||||
|
||||
final_message_text_in_chat = ""
|
||||
show_trial_button_after_action = False
|
||||
@@ -245,9 +228,7 @@ async def confirm_activate_trial_handler(
|
||||
)
|
||||
config_link_for_trial = config_link_display_for_trial or _("config_link_not_available")
|
||||
|
||||
traffic_gb_val = activation_result.get(
|
||||
"traffic_gb", settings.TRIAL_TRAFFIC_LIMIT_GB
|
||||
)
|
||||
traffic_gb_val = activation_result.get("traffic_gb", settings.TRIAL_TRAFFIC_LIMIT_GB)
|
||||
traffic_display = (
|
||||
f"{traffic_gb_val} GB"
|
||||
if traffic_gb_val and traffic_gb_val > 0
|
||||
@@ -258,9 +239,7 @@ async def confirm_activate_trial_handler(
|
||||
"trial_activated_details_message",
|
||||
days=activation_result.get("days", settings.TRIAL_DURATION_DAYS),
|
||||
end_date=(
|
||||
end_date_obj.strftime("%Y-%m-%d")
|
||||
if isinstance(end_date_obj, datetime)
|
||||
else "N/A"
|
||||
end_date_obj.strftime("%Y-%m-%d") if isinstance(end_date_obj, datetime) else "N/A"
|
||||
),
|
||||
config_link=config_link_for_trial,
|
||||
traffic_gb=traffic_display,
|
||||
@@ -276,11 +255,8 @@ async def confirm_activate_trial_handler(
|
||||
await callback.answer(final_message_text_in_chat, show_alert=True)
|
||||
except Exception:
|
||||
pass
|
||||
if (
|
||||
settings.TRIAL_ENABLED
|
||||
and not await subscription_service.has_had_any_subscription(
|
||||
session, user_id
|
||||
)
|
||||
if settings.TRIAL_ENABLED and not await subscription_service.has_had_any_subscription(
|
||||
session, user_id
|
||||
):
|
||||
show_trial_button_after_action = True
|
||||
|
||||
@@ -306,9 +282,7 @@ async def confirm_activate_trial_handler(
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
except Exception as e_edit:
|
||||
logging.warning(
|
||||
f"Could not edit trial result message: {e_edit}. Sending new one."
|
||||
)
|
||||
logging.warning(f"Could not edit trial result message: {e_edit}. Sending new one.")
|
||||
|
||||
if callback.message:
|
||||
await callback.message.answer(
|
||||
@@ -323,6 +297,7 @@ async def confirm_activate_trial_handler(
|
||||
await notification_service.notify_trial_activation(user_id, end_date_obj)
|
||||
try:
|
||||
from db.dal import ad_dal as _ad_dal
|
||||
|
||||
await _ad_dal.mark_trial_activated(session, user_id)
|
||||
await session.commit()
|
||||
except Exception as e_mark:
|
||||
@@ -338,6 +313,4 @@ async def cancel_trial_activation(
|
||||
subscription_service: SubscriptionService,
|
||||
session: AsyncSession,
|
||||
):
|
||||
await send_main_menu(
|
||||
callback, settings, i18n_data, subscription_service, session, is_edit=True
|
||||
)
|
||||
await send_main_menu(callback, settings, i18n_data, subscription_service, session, is_edit=True)
|
||||
|
||||
Reference in New Issue
Block a user