added gb packets selling
This commit is contained in:
@@ -104,6 +104,10 @@ RUB_PRICE_12_MONTHS=900
|
|||||||
STARS_PRICE_12_MONTHS=0
|
STARS_PRICE_12_MONTHS=0
|
||||||
TRIBUTE_LINK_12_MONTHS=
|
TRIBUTE_LINK_12_MONTHS=
|
||||||
|
|
||||||
|
# Traffic Packages (enables traffic sale mode when set)
|
||||||
|
TRAFFIC_PACKAGES=10:199,50:799 # Format: "<GB>:<price>", comma-separated
|
||||||
|
STARS_TRAFFIC_PACKAGES=10:2500 # Optional: traffic packages priced in Stars
|
||||||
|
|
||||||
# Subscription Notifications
|
# Subscription Notifications
|
||||||
SUBSCRIPTION_NOTIFICATIONS_ENABLED=True # Enable subscription
|
SUBSCRIPTION_NOTIFICATIONS_ENABLED=True # Enable subscription
|
||||||
SUBSCRIPTION_NOTIFY_ON_EXPIRE=True # Notify on subscription
|
SUBSCRIPTION_NOTIFY_ON_EXPIRE=True # Notify on subscription
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ async def get_payments_with_pagination(session: AsyncSession, page: int = 0,
|
|||||||
return payments, total_count
|
return payments, total_count
|
||||||
|
|
||||||
|
|
||||||
def format_payment_text(payment: Payment, i18n: JsonI18n, lang: str) -> str:
|
def format_payment_text(payment: Payment, i18n: JsonI18n, lang: str, settings: Settings) -> str:
|
||||||
"""Format single payment info as text."""
|
"""Format single payment info as text."""
|
||||||
_ = lambda key, **kwargs: i18n.gettext(lang, key, **kwargs)
|
_ = lambda key, **kwargs: i18n.gettext(lang, key, **kwargs)
|
||||||
|
|
||||||
@@ -66,12 +66,21 @@ def format_payment_text(payment: Payment, i18n: JsonI18n, lang: str) -> str:
|
|||||||
'severpay': 'SeverPay',
|
'severpay': 'SeverPay',
|
||||||
'platega': 'Platega',
|
'platega': 'Platega',
|
||||||
}.get(payment.provider, payment.provider or 'Unknown')
|
}.get(payment.provider, payment.provider or 'Unknown')
|
||||||
|
|
||||||
|
traffic_mode = getattr(settings, "traffic_sale_mode", False)
|
||||||
|
if traffic_mode:
|
||||||
|
traffic_val = payment.subscription_duration_months or 0
|
||||||
|
traffic_display = str(int(traffic_val)) if float(traffic_val).is_integer() else f"{traffic_val:g}"
|
||||||
|
period_line = _("admin_payment_traffic_label", default="🗂 Трафик: <b>{traffic_gb} GB</b>", traffic_gb=traffic_display)
|
||||||
|
else:
|
||||||
|
period_line = _("admin_payment_months_label", default="📅 Период: <b>{months} мес.</b>", months=payment.subscription_duration_months or 0)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
f"{status_emoji} <b>{payment.amount} {payment.currency}</b>\n"
|
f"{status_emoji} <b>{payment.amount} {payment.currency}</b>\n"
|
||||||
f"👤 {user_info}\n"
|
f"👤 {user_info}\n"
|
||||||
f"💳 {provider_text}\n"
|
f"💳 {provider_text}\n"
|
||||||
f"📅 {payment_date}\n"
|
f"📅 {payment_date}\n"
|
||||||
|
f"{period_line}\n"
|
||||||
f"📋 {payment.status}\n"
|
f"📋 {payment.status}\n"
|
||||||
f"📝 {payment.description or 'N/A'}"
|
f"📝 {payment.description or 'N/A'}"
|
||||||
)
|
)
|
||||||
@@ -109,7 +118,7 @@ async def view_payments_handler(callback: types.CallbackQuery, i18n_data: dict,
|
|||||||
total_pages=total_pages) + "\n")
|
total_pages=total_pages) + "\n")
|
||||||
|
|
||||||
for i, payment in enumerate(payments, 1):
|
for i, payment in enumerate(payments, 1):
|
||||||
text_parts.append(f"<b>{page * page_size + i}.</b> {format_payment_text(payment, i18n, current_lang)}")
|
text_parts.append(f"<b>{page * page_size + i}.</b> {format_payment_text(payment, i18n, current_lang, settings)}")
|
||||||
text_parts.append("") # Empty line between payments
|
text_parts.append("") # Empty line between payments
|
||||||
|
|
||||||
# Build keyboard with pagination and export
|
# Build keyboard with pagination and export
|
||||||
@@ -202,13 +211,21 @@ async def export_payments_csv_handler(callback: types.CallbackQuery, i18n_data:
|
|||||||
_("admin_csv_provider", default="Provider"),
|
_("admin_csv_provider", default="Provider"),
|
||||||
_("admin_csv_status", default="Status"),
|
_("admin_csv_status", default="Status"),
|
||||||
_("admin_csv_description", default="Description"),
|
_("admin_csv_description", default="Description"),
|
||||||
_("admin_csv_months", default="Months"),
|
_("admin_csv_units", default="Months/GB"),
|
||||||
_("admin_csv_created_at", default="Created At"),
|
_("admin_csv_created_at", default="Created At"),
|
||||||
_("admin_csv_provider_payment_id", default="Provider Payment ID")
|
_("admin_csv_provider_payment_id", default="Provider Payment ID")
|
||||||
])
|
])
|
||||||
|
|
||||||
|
traffic_mode = getattr(settings, "traffic_sale_mode", False)
|
||||||
|
|
||||||
# Write payment data
|
# Write payment data
|
||||||
for payment in all_payments:
|
for payment in all_payments:
|
||||||
|
units_val = payment.subscription_duration_months or ""
|
||||||
|
if traffic_mode and units_val not in ("", None):
|
||||||
|
try:
|
||||||
|
units_val = str(int(units_val)) if float(units_val).is_integer() else f"{units_val:g}"
|
||||||
|
except Exception:
|
||||||
|
units_val = payment.subscription_duration_months or ""
|
||||||
writer.writerow([
|
writer.writerow([
|
||||||
payment.payment_id,
|
payment.payment_id,
|
||||||
payment.user_id,
|
payment.user_id,
|
||||||
@@ -219,7 +236,7 @@ async def export_payments_csv_handler(callback: types.CallbackQuery, i18n_data:
|
|||||||
payment.provider or "",
|
payment.provider or "",
|
||||||
payment.status,
|
payment.status,
|
||||||
payment.description or "",
|
payment.description or "",
|
||||||
payment.subscription_duration_months or "",
|
units_val,
|
||||||
payment.created_at.strftime('%Y-%m-%d %H:%M:%S') if payment.created_at else "",
|
payment.created_at.strftime('%Y-%m-%d %H:%M:%S') if payment.created_at else "",
|
||||||
payment.provider_payment_id or ""
|
payment.provider_payment_id or ""
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
|||||||
metadata = payment_info_from_webhook.get("metadata", {})
|
metadata = payment_info_from_webhook.get("metadata", {})
|
||||||
user_id_str = metadata.get("user_id")
|
user_id_str = metadata.get("user_id")
|
||||||
subscription_months_str = metadata.get("subscription_months")
|
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")
|
||||||
promo_code_id_str = metadata.get("promo_code_id")
|
promo_code_id_str = metadata.get("promo_code_id")
|
||||||
payment_db_id_str = metadata.get("payment_db_id")
|
payment_db_id_str = metadata.get("payment_db_id")
|
||||||
auto_renew_subscription_id_str = metadata.get(
|
auto_renew_subscription_id_str = metadata.get(
|
||||||
@@ -47,8 +49,11 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
|||||||
|
|
||||||
# For auto-renew payments, payment_db_id may be absent. In that case,
|
# 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.
|
# we will create/ensure a payment record idempotently using provider payment id.
|
||||||
if (not user_id_str or not subscription_months_str
|
if (
|
||||||
or (not payment_db_id_str and not auto_renew_subscription_id_str)):
|
not user_id_str
|
||||||
|
or (not subscription_months_str and not traffic_gb_str)
|
||||||
|
or (not payment_db_id_str and not auto_renew_subscription_id_str)
|
||||||
|
):
|
||||||
logging.error(
|
logging.error(
|
||||||
f"Missing crucial metadata for payment: {payment_info_from_webhook.get('id')}, metadata: {metadata}"
|
f"Missing crucial metadata for payment: {payment_info_from_webhook.get('id')}, metadata: {metadata}"
|
||||||
)
|
)
|
||||||
@@ -57,15 +62,17 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
|||||||
db_user = None
|
db_user = None
|
||||||
try:
|
try:
|
||||||
user_id = int(user_id_str)
|
user_id = int(user_id_str)
|
||||||
subscription_months = int(subscription_months_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 = int(
|
||||||
payment_db_id_str) if payment_db_id_str and payment_db_id_str.isdigit() else None
|
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)
|
is_auto_renew = bool(auto_renew_subscription_id_str and not payment_db_id and sale_mode != "traffic")
|
||||||
promo_code_id = int(
|
promo_code_id = int(
|
||||||
promo_code_id_str
|
promo_code_id_str
|
||||||
) if promo_code_id_str and promo_code_id_str.isdigit() else None
|
) if promo_code_id_str and promo_code_id_str.isdigit() else None
|
||||||
|
|
||||||
amount_data = payment_info_from_webhook.get("amount", {})
|
amount_data = payment_info_from_webhook.get("amount", {})
|
||||||
|
months_for_record = int(subscription_months) if sale_mode != "traffic" else 0
|
||||||
payment_value = float(amount_data.get("value", 0.0))
|
payment_value = float(amount_data.get("value", 0.0))
|
||||||
|
|
||||||
# If this is an auto-renewal (no payment_db_id in metadata), ensure a payment record exists
|
# If this is an auto-renewal (no payment_db_id in metadata), ensure a payment record exists
|
||||||
@@ -79,9 +86,9 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
|||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
amount=payment_value,
|
amount=payment_value,
|
||||||
currency=amount_data.get("currency", settings.DEFAULT_CURRENCY_SYMBOL),
|
currency=amount_data.get("currency", settings.DEFAULT_CURRENCY_SYMBOL),
|
||||||
months=subscription_months,
|
months=months_for_record or 1,
|
||||||
description=payment_info_from_webhook.get(
|
description=payment_info_from_webhook.get(
|
||||||
"description") or f"Auto-renewal for {subscription_months} months",
|
"description") or f"Auto-renewal for {months_for_record or subscription_months} months",
|
||||||
provider="yookassa",
|
provider="yookassa",
|
||||||
provider_payment_id=yk_payment_id_from_hook,
|
provider_payment_id=yk_payment_id_from_hook,
|
||||||
)
|
)
|
||||||
@@ -196,14 +203,18 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
|||||||
raise Exception(
|
raise Exception(
|
||||||
f"DB Error: Could not update payment record {payment_db_id}")
|
f"DB Error: Could not update payment record {payment_db_id}")
|
||||||
|
|
||||||
|
months_for_activation = int(subscription_months) if sale_mode != "traffic" else 0
|
||||||
activation_details = await subscription_service.activate_subscription(
|
activation_details = await subscription_service.activate_subscription(
|
||||||
session,
|
session,
|
||||||
user_id,
|
user_id,
|
||||||
subscription_months,
|
months_for_activation,
|
||||||
payment_value,
|
payment_value,
|
||||||
payment_db_id,
|
payment_db_id,
|
||||||
promo_code_id_from_payment=promo_code_id,
|
promo_code_id_from_payment=promo_code_id,
|
||||||
provider="yookassa")
|
provider="yookassa",
|
||||||
|
sale_mode=sale_mode,
|
||||||
|
traffic_gb=traffic_amount_gb if sale_mode == "traffic" 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(
|
logging.error(
|
||||||
@@ -217,13 +228,15 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
|||||||
applied_promo_bonus_days = activation_details.get(
|
applied_promo_bonus_days = activation_details.get(
|
||||||
"applied_promo_bonus_days", 0)
|
"applied_promo_bonus_days", 0)
|
||||||
|
|
||||||
referral_bonus_info = await referral_service.apply_referral_bonuses_for_payment(
|
referral_bonus_info = None
|
||||||
session,
|
if sale_mode != "traffic":
|
||||||
user_id,
|
referral_bonus_info = await referral_service.apply_referral_bonuses_for_payment(
|
||||||
subscription_months,
|
session,
|
||||||
current_payment_db_id=payment_db_id,
|
user_id,
|
||||||
skip_if_active_before_payment=False,
|
months_for_activation or int(subscription_months) or 1,
|
||||||
)
|
current_payment_db_id=payment_db_id,
|
||||||
|
skip_if_active_before_payment=False,
|
||||||
|
)
|
||||||
applied_referee_bonus_days_from_referral: Optional[int] = None
|
applied_referee_bonus_days_from_referral: Optional[int] = None
|
||||||
if referral_bonus_info and referral_bonus_info.get(
|
if referral_bonus_info and referral_bonus_info.get(
|
||||||
"referee_new_end_date"):
|
"referee_new_end_date"):
|
||||||
@@ -236,14 +249,28 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
|||||||
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)
|
_ = 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}"
|
||||||
|
)
|
||||||
# For auto-renew charges, avoid re-sending config link; send concise message
|
# For auto-renew charges, avoid re-sending config link; send concise message
|
||||||
if is_auto_renew and final_end_date_for_user:
|
if sale_mode != "traffic" and is_auto_renew and final_end_date_for_user:
|
||||||
details_message = _(
|
details_message = _(
|
||||||
"yookassa_auto_renewal",
|
"yookassa_auto_renewal",
|
||||||
months=subscription_months,
|
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
|
details_markup = None
|
||||||
|
elif sale_mode == "traffic":
|
||||||
|
config_link = activation_details.get("subscription_url") or _("config_link_not_available")
|
||||||
|
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 "—",
|
||||||
|
config_link=config_link,
|
||||||
|
)
|
||||||
|
details_markup = get_connect_and_main_keyboard(
|
||||||
|
user_lang, i18n, settings, config_link, preserve_message=True
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
config_link = activation_details.get("subscription_url") or _(
|
config_link = activation_details.get("subscription_url") or _(
|
||||||
"config_link_not_available"
|
"config_link_not_available"
|
||||||
@@ -263,7 +290,7 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
|||||||
|
|
||||||
details_message = _(
|
details_message = _(
|
||||||
"payment_successful_with_referral_bonus_full",
|
"payment_successful_with_referral_bonus_full",
|
||||||
months=subscription_months,
|
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,
|
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'),
|
||||||
@@ -273,7 +300,7 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
|||||||
elif applied_promo_bonus_days > 0 and final_end_date_for_user:
|
elif applied_promo_bonus_days > 0 and final_end_date_for_user:
|
||||||
details_message = _(
|
details_message = _(
|
||||||
"payment_successful_with_promo_full",
|
"payment_successful_with_promo_full",
|
||||||
months=subscription_months,
|
months=int(subscription_months),
|
||||||
bonus_days=applied_promo_bonus_days,
|
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,
|
config_link=config_link,
|
||||||
@@ -281,7 +308,7 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
|||||||
elif final_end_date_for_user:
|
elif final_end_date_for_user:
|
||||||
details_message = _(
|
details_message = _(
|
||||||
"payment_successful_full",
|
"payment_successful_full",
|
||||||
months=subscription_months,
|
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,
|
config_link=config_link,
|
||||||
)
|
)
|
||||||
@@ -315,9 +342,10 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
|||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
amount=payment_value,
|
amount=payment_value,
|
||||||
currency=settings.DEFAULT_CURRENCY_SYMBOL,
|
currency=settings.DEFAULT_CURRENCY_SYMBOL,
|
||||||
months=subscription_months,
|
months=int(subscription_months) if sale_mode != "traffic" else 0,
|
||||||
payment_provider="yookassa", # This is specifically for YooKassa webhook
|
payment_provider="yookassa", # This is specifically for YooKassa webhook
|
||||||
username=user.username if user else None
|
username=user.username if user else None,
|
||||||
|
traffic_gb=traffic_amount_gb if sale_mode == "traffic" else None,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Failed to send payment notification: {e}")
|
logging.error(f"Failed to send payment notification: {e}")
|
||||||
|
|||||||
@@ -74,24 +74,26 @@ async def referral_command_handler(event: Union[types.Message,
|
|||||||
return
|
return
|
||||||
|
|
||||||
bonus_info_parts = []
|
bonus_info_parts = []
|
||||||
if settings.subscription_options:
|
if getattr(settings, "traffic_sale_mode", False):
|
||||||
|
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(
|
inv_bonus = settings.referral_bonus_inviter.get(months_period_key)
|
||||||
settings.subscription_options.items()):
|
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")))
|
||||||
|
|
||||||
inv_bonus = settings.referral_bonus_inviter.get(months_period_key)
|
bonus_details_str = "\n".join(bonus_info_parts) if bonus_info_parts else _(
|
||||||
ref_bonus = settings.referral_bonus_referee.get(months_period_key)
|
"referral_no_bonuses_configured")
|
||||||
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")))
|
|
||||||
|
|
||||||
bonus_details_str = "\n".join(bonus_info_parts) if bonus_info_parts else _(
|
|
||||||
"referral_no_bonuses_configured")
|
|
||||||
|
|
||||||
# Get referral statistics
|
# Get referral statistics
|
||||||
referral_stats = await referral_service.get_referral_stats(session, inviter_user_id)
|
referral_stats = await referral_service.get_referral_stats(session, inviter_user_id)
|
||||||
|
|||||||
@@ -40,11 +40,15 @@ async def display_subscription_options(event: Union[types.Message, types.Callbac
|
|||||||
return
|
return
|
||||||
|
|
||||||
currency_symbol_val = settings.DEFAULT_CURRENCY_SYMBOL
|
currency_symbol_val = settings.DEFAULT_CURRENCY_SYMBOL
|
||||||
text_content = get_text("select_subscription_period") if settings.subscription_options else get_text("no_subscription_options_available")
|
traffic_mode = bool(getattr(settings, "traffic_sale_mode", False))
|
||||||
|
options = settings.traffic_packages if traffic_mode else settings.subscription_options
|
||||||
|
text_content = (
|
||||||
|
get_text("select_traffic_package") if traffic_mode else get_text("select_subscription_period")
|
||||||
|
) if options else get_text("no_subscription_options_available")
|
||||||
|
|
||||||
reply_markup = (
|
reply_markup = (
|
||||||
get_subscription_options_keyboard(settings.subscription_options, currency_symbol_val, current_lang, i18n)
|
get_subscription_options_keyboard(options, currency_symbol_val, current_lang, i18n, traffic_mode=traffic_mode)
|
||||||
if settings.subscription_options
|
if options
|
||||||
else get_back_to_main_menu_markup(current_lang, i18n)
|
else get_back_to_main_menu_markup(current_lang, i18n)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -125,17 +129,50 @@ async def my_subscription_command_handler(
|
|||||||
|
|
||||||
end_date = active.get("end_date")
|
end_date = active.get("end_date")
|
||||||
days_left = (end_date.date() - datetime.now().date()).days if end_date else 0
|
days_left = (end_date.date() - datetime.now().date()).days if end_date else 0
|
||||||
text = get_text(
|
traffic_mode = bool(getattr(settings, "traffic_sale_mode", False))
|
||||||
"my_subscription_details",
|
def _fmt_gb(val: Optional[float]) -> str:
|
||||||
end_date=end_date.strftime("%Y-%m-%d") if end_date else "N/A",
|
if val is None:
|
||||||
days_left=max(0, days_left),
|
return get_text("traffic_na")
|
||||||
status=active.get("status_from_panel", get_text("status_active")).capitalize(),
|
try:
|
||||||
config_link=active.get("config_link") or get_text("config_link_not_available"),
|
if isinstance(val, (int, float)):
|
||||||
traffic_limit=(f"{active['traffic_limit_bytes'] / 2**30:.2f} GB" if active.get("traffic_limit_bytes") else get_text("traffic_unlimited")),
|
val_gb = float(val) / (2**30)
|
||||||
traffic_used=(
|
return f"{val_gb:.2f} GB"
|
||||||
f"{active['traffic_used_bytes'] / 2**30:.2f} GB" if active.get("traffic_used_bytes") is not None else get_text("traffic_na")
|
except Exception:
|
||||||
),
|
pass
|
||||||
)
|
return str(val)
|
||||||
|
|
||||||
|
if traffic_mode:
|
||||||
|
limit_display = _fmt_gb(active.get("traffic_limit_bytes"))
|
||||||
|
used_display = _fmt_gb(active.get("traffic_used_bytes"))
|
||||||
|
remaining_display = get_text("traffic_na")
|
||||||
|
try:
|
||||||
|
limit_val = active.get("traffic_limit_bytes") or 0
|
||||||
|
used_val = active.get("traffic_used_bytes") or 0
|
||||||
|
remaining_val = max(0, float(limit_val) - float(used_val))
|
||||||
|
remaining_display = _fmt_gb(remaining_val)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
text = get_text(
|
||||||
|
"my_traffic_details",
|
||||||
|
status=active.get("status_from_panel", get_text("status_active")).capitalize(),
|
||||||
|
end_date=end_date.strftime("%Y-%m-%d") if end_date else get_text("traffic_no_expiry"),
|
||||||
|
traffic_limit=limit_display,
|
||||||
|
traffic_used=used_display,
|
||||||
|
traffic_left=remaining_display,
|
||||||
|
config_link=active.get("config_link") or get_text("config_link_not_available"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
text = get_text(
|
||||||
|
"my_subscription_details",
|
||||||
|
end_date=end_date.strftime("%Y-%m-%d") if end_date else "N/A",
|
||||||
|
days_left=max(0, days_left),
|
||||||
|
status=active.get("status_from_panel", get_text("status_active")).capitalize(),
|
||||||
|
config_link=active.get("config_link") or get_text("config_link_not_available"),
|
||||||
|
traffic_limit=(f"{active['traffic_limit_bytes'] / 2**30:.2f} GB" if active.get("traffic_limit_bytes") else get_text("traffic_unlimited")),
|
||||||
|
traffic_used=(
|
||||||
|
f"{active['traffic_used_bytes'] / 2**30:.2f} GB" if active.get("traffic_used_bytes") is not None else get_text("traffic_na")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
base_markup = get_back_to_main_menu_markup(current_lang, i18n)
|
base_markup = get_back_to_main_menu_markup(current_lang, i18n)
|
||||||
kb = base_markup.inline_keyboard
|
kb = base_markup.inline_keyboard
|
||||||
@@ -214,7 +251,7 @@ async def my_subscription_command_handler(
|
|||||||
])
|
])
|
||||||
|
|
||||||
# 2) Auto-renew toggle (YooKassa only)
|
# 2) Auto-renew toggle (YooKassa only)
|
||||||
if 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 = (
|
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")
|
||||||
)
|
)
|
||||||
@@ -226,7 +263,7 @@ async def my_subscription_command_handler(
|
|||||||
])
|
])
|
||||||
|
|
||||||
# 3) Payment methods management (when autopayments enabled)
|
# 3) Payment methods management (when autopayments enabled)
|
||||||
if settings.yookassa_autopayments_active:
|
if not traffic_mode and settings.yookassa_autopayments_active:
|
||||||
prepend_rows.append([
|
prepend_rows.append([
|
||||||
InlineKeyboardButton(text=get_text("payment_methods_manage_button"), callback_data="pm:manage")
|
InlineKeyboardButton(text=get_text("payment_methods_manage_button"), callback_data="pm:manage")
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -390,8 +390,15 @@ async def payment_method_history(callback: types.CallbackQuery, settings: Settin
|
|||||||
await callback.message.edit_text(_("payment_method_no_history"), reply_markup=back_markup)
|
await callback.message.edit_text(_("payment_method_no_history"), reply_markup=back_markup)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
traffic_mode = getattr(settings, "traffic_sale_mode", False)
|
||||||
|
|
||||||
def _format_item(p: Payment) -> str:
|
def _format_item(p: Payment) -> str:
|
||||||
title = p.description or _("subscription_purchase_title", months=p.subscription_duration_months or 1)
|
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}"
|
||||||
|
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"
|
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}"
|
return f"{date_str} — {title} — {p.amount:.2f} {p.currency}"
|
||||||
|
|
||||||
@@ -455,4 +462,3 @@ async def payment_methods_list(callback: types.CallbackQuery, settings: Settings
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -39,9 +39,10 @@ async def pay_crypto_callback_handler(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
_, data_payload = callback.data.split(":", 1)
|
_, data_payload = callback.data.split(":", 1)
|
||||||
months_str, price_str = data_payload.split(":")
|
parts = data_payload.split(":")
|
||||||
months = int(months_str)
|
months = float(parts[0])
|
||||||
price_amount = float(price_str)
|
price_amount = float(parts[1])
|
||||||
|
sale_mode = parts[2] if len(parts) > 2 else "subscription"
|
||||||
except (ValueError, IndexError):
|
except (ValueError, IndexError):
|
||||||
try:
|
try:
|
||||||
await callback.answer(get_text("error_try_again"), show_alert=True)
|
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||||
@@ -50,7 +51,12 @@ async def pay_crypto_callback_handler(
|
|||||||
return
|
return
|
||||||
|
|
||||||
user_id = callback.from_user.id
|
user_id = callback.from_user.id
|
||||||
payment_description = get_text("payment_description_subscription", months=months)
|
human_value = str(int(months)) if float(months).is_integer() else f"{months:g}"
|
||||||
|
payment_description = (
|
||||||
|
get_text("payment_description_traffic", traffic_gb=human_value)
|
||||||
|
if sale_mode == "traffic"
|
||||||
|
else get_text("payment_description_subscription", months=int(months))
|
||||||
|
)
|
||||||
|
|
||||||
invoice_url = await cryptopay_service.create_invoice(
|
invoice_url = await cryptopay_service.create_invoice(
|
||||||
session=session,
|
session=session,
|
||||||
@@ -58,17 +64,22 @@ async def pay_crypto_callback_handler(
|
|||||||
months=months,
|
months=months,
|
||||||
amount=price_amount,
|
amount=price_amount,
|
||||||
description=payment_description,
|
description=payment_description,
|
||||||
|
sale_mode=sale_mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
if invoice_url:
|
if invoice_url:
|
||||||
try:
|
try:
|
||||||
await callback.message.edit_text(
|
await callback.message.edit_text(
|
||||||
get_text(key="payment_link_message", months=months),
|
get_text(
|
||||||
|
key="payment_link_message_traffic" if sale_mode == "traffic" else "payment_link_message",
|
||||||
|
months=int(months),
|
||||||
|
traffic_gb=human_value,
|
||||||
|
),
|
||||||
reply_markup=get_payment_url_keyboard(
|
reply_markup=get_payment_url_keyboard(
|
||||||
invoice_url,
|
invoice_url,
|
||||||
current_lang,
|
current_lang,
|
||||||
i18n,
|
i18n,
|
||||||
back_callback=f"subscribe_period:{months}",
|
back_callback=f"subscribe_period:{human_value}",
|
||||||
back_text_key="back_to_payment_methods_button",
|
back_text_key="back_to_payment_methods_button",
|
||||||
),
|
),
|
||||||
disable_web_page_preview=False,
|
disable_web_page_preview=False,
|
||||||
@@ -76,12 +87,16 @@ async def pay_crypto_callback_handler(
|
|||||||
except Exception:
|
except Exception:
|
||||||
try:
|
try:
|
||||||
await callback.message.answer(
|
await callback.message.answer(
|
||||||
get_text(key="payment_link_message", months=months),
|
get_text(
|
||||||
|
key="payment_link_message_traffic" if sale_mode == "traffic" else "payment_link_message",
|
||||||
|
months=int(months),
|
||||||
|
traffic_gb=human_value,
|
||||||
|
),
|
||||||
reply_markup=get_payment_url_keyboard(
|
reply_markup=get_payment_url_keyboard(
|
||||||
invoice_url,
|
invoice_url,
|
||||||
current_lang,
|
current_lang,
|
||||||
i18n,
|
i18n,
|
||||||
back_callback=f"subscribe_period:{months}",
|
back_callback=f"subscribe_period:{human_value}",
|
||||||
back_text_key="back_to_payment_methods_button",
|
back_text_key="back_to_payment_methods_button",
|
||||||
),
|
),
|
||||||
disable_web_page_preview=False,
|
disable_web_page_preview=False,
|
||||||
|
|||||||
@@ -47,9 +47,10 @@ async def pay_fk_callback_handler(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
_, data_payload = callback.data.split(":", 1)
|
_, data_payload = callback.data.split(":", 1)
|
||||||
months_str, price_str = data_payload.split(":")
|
parts = data_payload.split(":")
|
||||||
months = int(months_str)
|
months = float(parts[0])
|
||||||
price_rub = float(price_str)
|
price_rub = float(parts[1])
|
||||||
|
sale_mode = parts[2] if len(parts) > 2 else "subscription"
|
||||||
except (ValueError, IndexError):
|
except (ValueError, IndexError):
|
||||||
logging.error(f"Invalid pay_fk data in callback: {callback.data}")
|
logging.error(f"Invalid pay_fk data in callback: {callback.data}")
|
||||||
try:
|
try:
|
||||||
@@ -59,7 +60,12 @@ async def pay_fk_callback_handler(
|
|||||||
return
|
return
|
||||||
|
|
||||||
user_id = callback.from_user.id
|
user_id = callback.from_user.id
|
||||||
payment_description = get_text("payment_description_subscription", months=months)
|
human_value = str(int(months)) if float(months).is_integer() else f"{months:g}"
|
||||||
|
payment_description = (
|
||||||
|
get_text("payment_description_traffic", traffic_gb=human_value)
|
||||||
|
if sale_mode == "traffic"
|
||||||
|
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 = {
|
payment_record_payload = {
|
||||||
@@ -68,7 +74,7 @@ async def pay_fk_callback_handler(
|
|||||||
"currency": currency_code,
|
"currency": currency_code,
|
||||||
"status": "pending_freekassa",
|
"status": "pending_freekassa",
|
||||||
"description": payment_description,
|
"description": payment_description,
|
||||||
"subscription_duration_months": months,
|
"subscription_duration_months": int(months),
|
||||||
"provider": "freekassa",
|
"provider": "freekassa",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,12 +141,16 @@ async def pay_fk_callback_handler(
|
|||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
await callback.message.edit_text(
|
await callback.message.edit_text(
|
||||||
f"{order_info_text}\n\n" + get_text(key="payment_link_message", months=months),
|
f"{order_info_text}\n\n" + get_text(
|
||||||
|
key="payment_link_message_traffic" if sale_mode == "traffic" else "payment_link_message",
|
||||||
|
months=int(months),
|
||||||
|
traffic_gb=human_value,
|
||||||
|
),
|
||||||
reply_markup=get_payment_url_keyboard(
|
reply_markup=get_payment_url_keyboard(
|
||||||
location,
|
location,
|
||||||
current_lang,
|
current_lang,
|
||||||
i18n,
|
i18n,
|
||||||
back_callback=f"subscribe_period:{months}",
|
back_callback=f"subscribe_period:{human_value}",
|
||||||
back_text_key="back_to_payment_methods_button",
|
back_text_key="back_to_payment_methods_button",
|
||||||
),
|
),
|
||||||
disable_web_page_preview=False,
|
disable_web_page_preview=False,
|
||||||
@@ -149,12 +159,16 @@ async def pay_fk_callback_handler(
|
|||||||
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:
|
try:
|
||||||
await callback.message.answer(
|
await callback.message.answer(
|
||||||
f"{order_info_text}\n\n" + get_text(key="payment_link_message", months=months),
|
f"{order_info_text}\n\n" + get_text(
|
||||||
|
key="payment_link_message_traffic" if sale_mode == "traffic" else "payment_link_message",
|
||||||
|
months=int(months),
|
||||||
|
traffic_gb=human_value,
|
||||||
|
),
|
||||||
reply_markup=get_payment_url_keyboard(
|
reply_markup=get_payment_url_keyboard(
|
||||||
location,
|
location,
|
||||||
current_lang,
|
current_lang,
|
||||||
i18n,
|
i18n,
|
||||||
back_callback=f"subscribe_period:{months}",
|
back_callback=f"subscribe_period:{human_value}",
|
||||||
back_text_key="back_to_payment_methods_button",
|
back_text_key="back_to_payment_methods_button",
|
||||||
),
|
),
|
||||||
disable_web_page_preview=False,
|
disable_web_page_preview=False,
|
||||||
|
|||||||
@@ -47,9 +47,10 @@ async def pay_platega_callback_handler(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
_, data_payload = callback.data.split(":", 1)
|
_, data_payload = callback.data.split(":", 1)
|
||||||
months_str, price_str = data_payload.split(":")
|
parts = data_payload.split(":")
|
||||||
months = int(months_str)
|
months = float(parts[0])
|
||||||
price_rub = float(price_str)
|
price_rub = float(parts[1])
|
||||||
|
sale_mode = parts[2] if len(parts) > 2 else "subscription"
|
||||||
except (ValueError, IndexError):
|
except (ValueError, IndexError):
|
||||||
logging.error(f"Invalid pay_platega data in callback: {callback.data}")
|
logging.error(f"Invalid pay_platega data in callback: {callback.data}")
|
||||||
try:
|
try:
|
||||||
@@ -59,7 +60,12 @@ async def pay_platega_callback_handler(
|
|||||||
return
|
return
|
||||||
|
|
||||||
user_id = callback.from_user.id
|
user_id = callback.from_user.id
|
||||||
payment_description = get_text("payment_description_subscription", months=months)
|
human_value = str(int(months)) if float(months).is_integer() else f"{months:g}"
|
||||||
|
payment_description = (
|
||||||
|
get_text("payment_description_traffic", traffic_gb=human_value)
|
||||||
|
if sale_mode == "traffic"
|
||||||
|
else get_text("payment_description_subscription", months=int(months))
|
||||||
|
)
|
||||||
currency_code = settings.DEFAULT_CURRENCY_SYMBOL or "RUB"
|
currency_code = settings.DEFAULT_CURRENCY_SYMBOL or "RUB"
|
||||||
|
|
||||||
payment_record_payload = {
|
payment_record_payload = {
|
||||||
@@ -68,7 +74,7 @@ async def pay_platega_callback_handler(
|
|||||||
"currency": currency_code,
|
"currency": currency_code,
|
||||||
"status": "pending_platega",
|
"status": "pending_platega",
|
||||||
"description": payment_description,
|
"description": payment_description,
|
||||||
"subscription_duration_months": months,
|
"subscription_duration_months": int(months),
|
||||||
"provider": "platega",
|
"provider": "platega",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +102,7 @@ async def pay_platega_callback_handler(
|
|||||||
"payment_db_id": payment_record.payment_id,
|
"payment_db_id": payment_record.payment_id,
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"months": months,
|
"months": months,
|
||||||
|
"sale_mode": sale_mode,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -136,12 +143,16 @@ async def pay_platega_callback_handler(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
await callback.message.edit_text(
|
await callback.message.edit_text(
|
||||||
get_text(key="payment_link_message", months=months),
|
get_text(
|
||||||
|
key="payment_link_message_traffic" if sale_mode == "traffic" else "payment_link_message",
|
||||||
|
months=int(months),
|
||||||
|
traffic_gb=human_value,
|
||||||
|
),
|
||||||
reply_markup=get_payment_url_keyboard(
|
reply_markup=get_payment_url_keyboard(
|
||||||
redirect_url,
|
redirect_url,
|
||||||
current_lang,
|
current_lang,
|
||||||
i18n,
|
i18n,
|
||||||
back_callback=f"subscribe_period:{months}",
|
back_callback=f"subscribe_period:{human_value}",
|
||||||
back_text_key="back_to_payment_methods_button",
|
back_text_key="back_to_payment_methods_button",
|
||||||
),
|
),
|
||||||
disable_web_page_preview=False,
|
disable_web_page_preview=False,
|
||||||
@@ -150,12 +161,16 @@ async def pay_platega_callback_handler(
|
|||||||
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:
|
try:
|
||||||
await callback.message.answer(
|
await callback.message.answer(
|
||||||
get_text(key="payment_link_message", months=months),
|
get_text(
|
||||||
|
key="payment_link_message_traffic" if sale_mode == "traffic" else "payment_link_message",
|
||||||
|
months=int(months),
|
||||||
|
traffic_gb=human_value,
|
||||||
|
),
|
||||||
reply_markup=get_payment_url_keyboard(
|
reply_markup=get_payment_url_keyboard(
|
||||||
redirect_url,
|
redirect_url,
|
||||||
current_lang,
|
current_lang,
|
||||||
i18n,
|
i18n,
|
||||||
back_callback=f"subscribe_period:{months}",
|
back_callback=f"subscribe_period:{human_value}",
|
||||||
back_text_key="back_to_payment_methods_button",
|
back_text_key="back_to_payment_methods_button",
|
||||||
),
|
),
|
||||||
disable_web_page_preview=False,
|
disable_web_page_preview=False,
|
||||||
|
|||||||
@@ -46,9 +46,10 @@ async def pay_severpay_callback_handler(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
_, data_payload = callback.data.split(":", 1)
|
_, data_payload = callback.data.split(":", 1)
|
||||||
months_str, price_str = data_payload.split(":")
|
parts = data_payload.split(":")
|
||||||
months = int(months_str)
|
months = float(parts[0])
|
||||||
price_rub = float(price_str)
|
price_rub = float(parts[1])
|
||||||
|
sale_mode = parts[2] if len(parts) > 2 else "subscription"
|
||||||
except (ValueError, IndexError):
|
except (ValueError, IndexError):
|
||||||
logging.error(f"Invalid pay_severpay data in callback: {callback.data}")
|
logging.error(f"Invalid pay_severpay data in callback: {callback.data}")
|
||||||
try:
|
try:
|
||||||
@@ -58,7 +59,12 @@ async def pay_severpay_callback_handler(
|
|||||||
return
|
return
|
||||||
|
|
||||||
user_id = callback.from_user.id
|
user_id = callback.from_user.id
|
||||||
payment_description = get_text("payment_description_subscription", months=months)
|
human_value = str(int(months)) if float(months).is_integer() else f"{months:g}"
|
||||||
|
payment_description = (
|
||||||
|
get_text("payment_description_traffic", traffic_gb=human_value)
|
||||||
|
if sale_mode == "traffic"
|
||||||
|
else get_text("payment_description_subscription", months=int(months))
|
||||||
|
)
|
||||||
currency_code = settings.DEFAULT_CURRENCY_SYMBOL or "RUB"
|
currency_code = settings.DEFAULT_CURRENCY_SYMBOL or "RUB"
|
||||||
|
|
||||||
payment_record_payload = {
|
payment_record_payload = {
|
||||||
@@ -67,7 +73,7 @@ async def pay_severpay_callback_handler(
|
|||||||
"currency": currency_code,
|
"currency": currency_code,
|
||||||
"status": "pending_severpay",
|
"status": "pending_severpay",
|
||||||
"description": payment_description,
|
"description": payment_description,
|
||||||
"subscription_duration_months": months,
|
"subscription_duration_months": int(months),
|
||||||
"provider": "severpay",
|
"provider": "severpay",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,12 +132,16 @@ async def pay_severpay_callback_handler(
|
|||||||
if payment_link:
|
if payment_link:
|
||||||
try:
|
try:
|
||||||
await callback.message.edit_text(
|
await callback.message.edit_text(
|
||||||
get_text(key="payment_link_message", months=months),
|
get_text(
|
||||||
|
key="payment_link_message_traffic" if sale_mode == "traffic" else "payment_link_message",
|
||||||
|
months=int(months),
|
||||||
|
traffic_gb=human_value,
|
||||||
|
),
|
||||||
reply_markup=get_payment_url_keyboard(
|
reply_markup=get_payment_url_keyboard(
|
||||||
payment_link,
|
payment_link,
|
||||||
current_lang,
|
current_lang,
|
||||||
i18n,
|
i18n,
|
||||||
back_callback=f"subscribe_period:{months}",
|
back_callback=f"subscribe_period:{human_value}",
|
||||||
back_text_key="back_to_payment_methods_button",
|
back_text_key="back_to_payment_methods_button",
|
||||||
),
|
),
|
||||||
disable_web_page_preview=False,
|
disable_web_page_preview=False,
|
||||||
@@ -140,12 +150,16 @@ async def pay_severpay_callback_handler(
|
|||||||
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:
|
try:
|
||||||
await callback.message.answer(
|
await callback.message.answer(
|
||||||
get_text(key="payment_link_message", months=months),
|
get_text(
|
||||||
|
key="payment_link_message_traffic" if sale_mode == "traffic" else "payment_link_message",
|
||||||
|
months=int(months),
|
||||||
|
traffic_gb=human_value,
|
||||||
|
),
|
||||||
reply_markup=get_payment_url_keyboard(
|
reply_markup=get_payment_url_keyboard(
|
||||||
payment_link,
|
payment_link,
|
||||||
current_lang,
|
current_lang,
|
||||||
i18n,
|
i18n,
|
||||||
back_callback=f"subscribe_period:{months}",
|
back_callback=f"subscribe_period:{human_value}",
|
||||||
back_text_key="back_to_payment_methods_button",
|
back_text_key="back_to_payment_methods_button",
|
||||||
),
|
),
|
||||||
disable_web_page_preview=False,
|
disable_web_page_preview=False,
|
||||||
|
|||||||
@@ -40,9 +40,10 @@ async def pay_stars_callback_handler(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
_, data_payload = callback.data.split(":", 1)
|
_, data_payload = callback.data.split(":", 1)
|
||||||
months_str, stars_price_str = data_payload.split(":")
|
parts = data_payload.split(":")
|
||||||
months = int(months_str)
|
months = float(parts[0])
|
||||||
stars_price = int(stars_price_str)
|
stars_price = int(float(parts[1]))
|
||||||
|
sale_mode = parts[2] if len(parts) > 2 else "subscription"
|
||||||
except (ValueError, IndexError):
|
except (ValueError, IndexError):
|
||||||
try:
|
try:
|
||||||
await callback.answer(get_text("error_try_again"), show_alert=True)
|
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||||
@@ -51,7 +52,12 @@ async def pay_stars_callback_handler(
|
|||||||
return
|
return
|
||||||
|
|
||||||
user_id = callback.from_user.id
|
user_id = callback.from_user.id
|
||||||
payment_description = get_text("payment_description_subscription", months=months)
|
human_value = str(int(months)) if float(months).is_integer() else f"{months:g}"
|
||||||
|
payment_description = (
|
||||||
|
get_text("payment_description_traffic", traffic_gb=human_value)
|
||||||
|
if sale_mode == "traffic"
|
||||||
|
else get_text("payment_description_subscription", months=int(months))
|
||||||
|
)
|
||||||
|
|
||||||
payment_db_id = await stars_service.create_invoice(
|
payment_db_id = await stars_service.create_invoice(
|
||||||
session=session,
|
session=session,
|
||||||
@@ -59,16 +65,21 @@ async def pay_stars_callback_handler(
|
|||||||
months=months,
|
months=months,
|
||||||
stars_price=stars_price,
|
stars_price=stars_price,
|
||||||
description=payment_description,
|
description=payment_description,
|
||||||
|
sale_mode=sale_mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
if payment_db_id:
|
if payment_db_id:
|
||||||
try:
|
try:
|
||||||
await callback.message.edit_text(
|
await callback.message.edit_text(
|
||||||
get_text("payment_invoice_sent_message", months=months),
|
get_text(
|
||||||
|
"payment_invoice_sent_message_traffic" if sale_mode == "traffic" else "payment_invoice_sent_message",
|
||||||
|
months=int(months),
|
||||||
|
traffic_gb=human_value,
|
||||||
|
),
|
||||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||||
[InlineKeyboardButton(
|
[InlineKeyboardButton(
|
||||||
text=get_text("back_to_payment_methods_button"),
|
text=get_text("back_to_payment_methods_button"),
|
||||||
callback_data=f"subscribe_period:{months}",
|
callback_data=f"subscribe_period:{human_value}",
|
||||||
)]
|
)]
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
@@ -106,9 +117,10 @@ async def handle_successful_stars_payment(
|
|||||||
payload = (message.successful_payment.invoice_payload
|
payload = (message.successful_payment.invoice_payload
|
||||||
if message and message.successful_payment else "")
|
if message and message.successful_payment else "")
|
||||||
try:
|
try:
|
||||||
payment_db_id_str, months_str = (payload or "").split(":", 1)
|
parts = (payload or "").split(":")
|
||||||
payment_db_id = int(payment_db_id_str)
|
payment_db_id = int(parts[0])
|
||||||
months = int(months_str)
|
months = float(parts[1]) if len(parts) > 1 else 0
|
||||||
|
sale_mode = parts[2] if len(parts) > 2 else "subscription"
|
||||||
except Exception:
|
except Exception:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -120,4 +132,5 @@ async def handle_successful_stars_payment(
|
|||||||
months=months,
|
months=months,
|
||||||
stars_amount=stars_amount,
|
stars_amount=stars_amount,
|
||||||
i18n_data=i18n_data,
|
i18n_data=i18n_data,
|
||||||
|
sale_mode=sale_mode,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -29,8 +29,9 @@ async def select_subscription_period_callback_handler(
|
|||||||
pass
|
pass
|
||||||
return
|
return
|
||||||
|
|
||||||
|
traffic_mode = bool(getattr(settings, "traffic_sale_mode", False))
|
||||||
try:
|
try:
|
||||||
months = int(callback.data.split(":")[-1])
|
months = float(callback.data.split(":")[-1])
|
||||||
except (ValueError, IndexError):
|
except (ValueError, IndexError):
|
||||||
logging.error(f"Invalid subscription period in callback_data: {callback.data}")
|
logging.error(f"Invalid subscription period in callback_data: {callback.data}")
|
||||||
try:
|
try:
|
||||||
@@ -39,10 +40,13 @@ async def select_subscription_period_callback_handler(
|
|||||||
pass
|
pass
|
||||||
return
|
return
|
||||||
|
|
||||||
price_rub = settings.subscription_options.get(months)
|
price_source = settings.traffic_packages if traffic_mode else settings.subscription_options
|
||||||
|
stars_price_source = settings.stars_traffic_packages if traffic_mode else settings.stars_subscription_options
|
||||||
|
|
||||||
|
price_rub = price_source.get(months)
|
||||||
if price_rub is None:
|
if price_rub is None:
|
||||||
logging.error(
|
logging.error(
|
||||||
f"Price not found for {months} months subscription period in settings.subscription_options."
|
f"Price not found for option {months} using {'traffic_packages' if traffic_mode else 'subscription_options'}."
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
await callback.answer(get_text("error_try_again"), show_alert=True)
|
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||||
@@ -51,8 +55,8 @@ async def select_subscription_period_callback_handler(
|
|||||||
return
|
return
|
||||||
|
|
||||||
currency_symbol_val = settings.DEFAULT_CURRENCY_SYMBOL
|
currency_symbol_val = settings.DEFAULT_CURRENCY_SYMBOL
|
||||||
text_content = get_text("choose_payment_method")
|
text_content = get_text("choose_payment_method_traffic") if traffic_mode else get_text("choose_payment_method")
|
||||||
stars_price = settings.stars_subscription_options.get(months)
|
stars_price = stars_price_source.get(months)
|
||||||
reply_markup = get_payment_method_keyboard(
|
reply_markup = get_payment_method_keyboard(
|
||||||
months,
|
months,
|
||||||
price_rub,
|
price_rub,
|
||||||
@@ -61,6 +65,7 @@ async def select_subscription_period_callback_handler(
|
|||||||
current_lang,
|
current_lang,
|
||||||
i18n,
|
i18n,
|
||||||
settings,
|
settings,
|
||||||
|
sale_mode="traffic" if traffic_mode else "subscription",
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -18,10 +18,17 @@ from db.dal import payment_dal, user_billing_dal
|
|||||||
router = Router(name="user_subscription_payments_yookassa_router")
|
router = Router(name="user_subscription_payments_yookassa_router")
|
||||||
|
|
||||||
|
|
||||||
def _parse_months_and_price(payload: str) -> Optional[Tuple[int, float]]:
|
def _format_value(val: float) -> str:
|
||||||
|
return str(int(val)) if float(val).is_integer() else f"{val:g}"
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_offer_payload(payload: str) -> Optional[Tuple[float, float, str]]:
|
||||||
try:
|
try:
|
||||||
months_str, price_str = payload.split(":")
|
parts = payload.split(":")
|
||||||
return int(months_str), float(price_str)
|
value = float(parts[0])
|
||||||
|
price = float(parts[1])
|
||||||
|
sale_mode = parts[2] if len(parts) > 2 else "subscription"
|
||||||
|
return value, price, sale_mode
|
||||||
except (ValueError, IndexError):
|
except (ValueError, IndexError):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -64,19 +71,24 @@ async def _initiate_yk_payment(
|
|||||||
back_callback: str,
|
back_callback: str,
|
||||||
payment_method_id: Optional[str] = None,
|
payment_method_id: Optional[str] = None,
|
||||||
selected_method_internal_id: Optional[int] = None,
|
selected_method_internal_id: Optional[int] = None,
|
||||||
|
sale_mode: str = "subscription",
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Create payment record and initiate YooKassa payment (new card or saved card)."""
|
"""Create payment record and initiate YooKassa payment (new card or saved card)."""
|
||||||
if not callback.message:
|
if not callback.message:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
payment_description = get_text("payment_description_subscription", months=months)
|
payment_description = (
|
||||||
|
get_text("payment_description_traffic", traffic_gb=_format_value(months))
|
||||||
|
if sale_mode == "traffic"
|
||||||
|
else get_text("payment_description_subscription", months=int(months))
|
||||||
|
)
|
||||||
payment_record_data = {
|
payment_record_data = {
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"amount": price_rub,
|
"amount": price_rub,
|
||||||
"currency": currency_code_for_yk,
|
"currency": currency_code_for_yk,
|
||||||
"status": "pending_yookassa",
|
"status": "pending_yookassa",
|
||||||
"description": payment_description,
|
"description": payment_description,
|
||||||
"subscription_duration_months": months,
|
"subscription_duration_months": int(months),
|
||||||
}
|
}
|
||||||
|
|
||||||
db_payment_record = None
|
db_payment_record = None
|
||||||
@@ -109,7 +121,10 @@ async def _initiate_yk_payment(
|
|||||||
"user_id": str(user_id),
|
"user_id": str(user_id),
|
||||||
"subscription_months": str(months),
|
"subscription_months": str(months),
|
||||||
"payment_db_id": str(db_payment_record.payment_id),
|
"payment_db_id": str(db_payment_record.payment_id),
|
||||||
|
"sale_mode": sale_mode,
|
||||||
}
|
}
|
||||||
|
if sale_mode == "traffic":
|
||||||
|
yookassa_metadata["traffic_gb"] = str(months)
|
||||||
if payment_method_id:
|
if payment_method_id:
|
||||||
yookassa_metadata["used_saved_payment_method_id"] = payment_method_id
|
yookassa_metadata["used_saved_payment_method_id"] = payment_method_id
|
||||||
|
|
||||||
@@ -198,7 +213,11 @@ async def _initiate_yk_payment(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
await callback.message.edit_text(
|
await callback.message.edit_text(
|
||||||
get_text(key="payment_link_message", months=months),
|
get_text(
|
||||||
|
key="payment_link_message_traffic" if sale_mode == "traffic" else "payment_link_message",
|
||||||
|
months=int(months),
|
||||||
|
traffic_gb=_format_value(months),
|
||||||
|
),
|
||||||
reply_markup=get_payment_url_keyboard(
|
reply_markup=get_payment_url_keyboard(
|
||||||
payment_response_yk["confirmation_url"],
|
payment_response_yk["confirmation_url"],
|
||||||
current_lang,
|
current_lang,
|
||||||
@@ -214,7 +233,11 @@ async def _initiate_yk_payment(
|
|||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
await callback.message.answer(
|
await callback.message.answer(
|
||||||
get_text(key="payment_link_message", months=months),
|
get_text(
|
||||||
|
key="payment_link_message_traffic" if sale_mode == "traffic" else "payment_link_message",
|
||||||
|
months=int(months),
|
||||||
|
traffic_gb=_format_value(months),
|
||||||
|
),
|
||||||
reply_markup=get_payment_url_keyboard(
|
reply_markup=get_payment_url_keyboard(
|
||||||
payment_response_yk["confirmation_url"],
|
payment_response_yk["confirmation_url"],
|
||||||
current_lang,
|
current_lang,
|
||||||
@@ -328,7 +351,7 @@ async def pay_yk_callback_handler(callback: types.CallbackQuery, settings: Setti
|
|||||||
pass
|
pass
|
||||||
return
|
return
|
||||||
|
|
||||||
parsed = _parse_months_and_price(data_payload)
|
parsed = _parse_offer_payload(data_payload)
|
||||||
if not parsed:
|
if not parsed:
|
||||||
logging.error(f"Invalid pay_yk payload structure: {callback.data}")
|
logging.error(f"Invalid pay_yk payload structure: {callback.data}")
|
||||||
try:
|
try:
|
||||||
@@ -337,10 +360,10 @@ async def pay_yk_callback_handler(callback: types.CallbackQuery, settings: Setti
|
|||||||
pass
|
pass
|
||||||
return
|
return
|
||||||
|
|
||||||
months, price_rub = parsed
|
months, price_rub, sale_mode = parsed
|
||||||
user_id = callback.from_user.id
|
user_id = callback.from_user.id
|
||||||
currency_code_for_yk = "RUB"
|
currency_code_for_yk = "RUB"
|
||||||
autopay_enabled = bool(settings.yookassa_autopayments_active)
|
autopay_enabled = bool(settings.yookassa_autopayments_active and sale_mode != "traffic" and not settings.traffic_sale_mode)
|
||||||
autopay_require_binding = bool(
|
autopay_require_binding = bool(
|
||||||
getattr(settings, 'YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING', True)
|
getattr(settings, 'YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING', True)
|
||||||
)
|
)
|
||||||
@@ -364,6 +387,7 @@ async def pay_yk_callback_handler(callback: types.CallbackQuery, settings: Setti
|
|||||||
current_lang,
|
current_lang,
|
||||||
i18n,
|
i18n,
|
||||||
has_saved_cards=True,
|
has_saved_cards=True,
|
||||||
|
sale_mode=sale_mode,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
except Exception as e_edit:
|
except Exception as e_edit:
|
||||||
@@ -377,6 +401,7 @@ async def pay_yk_callback_handler(callback: types.CallbackQuery, settings: Setti
|
|||||||
current_lang,
|
current_lang,
|
||||||
i18n,
|
i18n,
|
||||||
has_saved_cards=True,
|
has_saved_cards=True,
|
||||||
|
sale_mode=sale_mode,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -400,7 +425,8 @@ async def pay_yk_callback_handler(callback: types.CallbackQuery, settings: Setti
|
|||||||
price_rub=price_rub,
|
price_rub=price_rub,
|
||||||
currency_code_for_yk=currency_code_for_yk,
|
currency_code_for_yk=currency_code_for_yk,
|
||||||
save_payment_method=autopay_enabled and autopay_require_binding,
|
save_payment_method=autopay_enabled and autopay_require_binding,
|
||||||
back_callback=f"subscribe_period:{months}",
|
back_callback=f"subscribe_period:{_format_value(months)}",
|
||||||
|
sale_mode=sale_mode,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
@@ -443,7 +469,7 @@ async def pay_yk_new_card_handler(callback: types.CallbackQuery, settings: Setti
|
|||||||
pass
|
pass
|
||||||
return
|
return
|
||||||
|
|
||||||
parsed = _parse_months_and_price(data_payload)
|
parsed = _parse_offer_payload(data_payload)
|
||||||
if not parsed:
|
if not parsed:
|
||||||
logging.error(f"Invalid pay_yk_new payload structure: {callback.data}")
|
logging.error(f"Invalid pay_yk_new payload structure: {callback.data}")
|
||||||
try:
|
try:
|
||||||
@@ -452,10 +478,10 @@ async def pay_yk_new_card_handler(callback: types.CallbackQuery, settings: Setti
|
|||||||
pass
|
pass
|
||||||
return
|
return
|
||||||
|
|
||||||
months, price_rub = parsed
|
months, price_rub, sale_mode = parsed
|
||||||
user_id = callback.from_user.id
|
user_id = callback.from_user.id
|
||||||
currency_code_for_yk = "RUB"
|
currency_code_for_yk = "RUB"
|
||||||
autopay_enabled = bool(settings.yookassa_autopayments_active)
|
autopay_enabled = bool(settings.yookassa_autopayments_active and sale_mode != "traffic" and not settings.traffic_sale_mode)
|
||||||
autopay_require_binding = bool(
|
autopay_require_binding = bool(
|
||||||
getattr(settings, 'YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING', True)
|
getattr(settings, 'YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING', True)
|
||||||
)
|
)
|
||||||
@@ -473,7 +499,8 @@ async def pay_yk_new_card_handler(callback: types.CallbackQuery, settings: Setti
|
|||||||
price_rub=price_rub,
|
price_rub=price_rub,
|
||||||
currency_code_for_yk=currency_code_for_yk,
|
currency_code_for_yk=currency_code_for_yk,
|
||||||
save_payment_method=autopay_enabled and autopay_require_binding,
|
save_payment_method=autopay_enabled and autopay_require_binding,
|
||||||
back_callback=f"subscribe_period:{months}",
|
back_callback=f"subscribe_period:{_format_value(months)}",
|
||||||
|
sale_mode=sale_mode,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
@@ -494,14 +521,6 @@ async def pay_yk_saved_list_handler(callback: types.CallbackQuery, settings: Set
|
|||||||
pass
|
pass
|
||||||
return
|
return
|
||||||
|
|
||||||
autopay_enabled = bool(settings.yookassa_autopayments_active)
|
|
||||||
if not autopay_enabled:
|
|
||||||
try:
|
|
||||||
await callback.answer(get_text("error_try_again"), show_alert=True)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_, data_payload = callback.data.split(":", 1)
|
_, data_payload = callback.data.split(":", 1)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@@ -522,9 +541,10 @@ async def pay_yk_saved_list_handler(callback: types.CallbackQuery, settings: Set
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
months = int(parts[0])
|
months = float(parts[0])
|
||||||
price_rub = float(parts[1])
|
price_rub = float(parts[1])
|
||||||
page = int(parts[2]) if len(parts) > 2 else 0
|
page = int(parts[2]) if len(parts) > 2 else 0
|
||||||
|
sale_mode = parts[3] if len(parts) > 3 else "subscription"
|
||||||
except (ValueError, IndexError):
|
except (ValueError, IndexError):
|
||||||
logging.error(f"pay_yk_saved_list payload parsing error: {callback.data}")
|
logging.error(f"pay_yk_saved_list payload parsing error: {callback.data}")
|
||||||
try:
|
try:
|
||||||
@@ -533,6 +553,14 @@ async def pay_yk_saved_list_handler(callback: types.CallbackQuery, settings: Set
|
|||||||
pass
|
pass
|
||||||
return
|
return
|
||||||
|
|
||||||
|
autopay_enabled = bool(settings.yookassa_autopayments_active and sale_mode != "traffic" and not settings.traffic_sale_mode)
|
||||||
|
if not autopay_enabled:
|
||||||
|
try:
|
||||||
|
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
|
||||||
user_id = callback.from_user.id
|
user_id = callback.from_user.id
|
||||||
try:
|
try:
|
||||||
saved_methods = await user_billing_dal.list_user_payment_methods(
|
saved_methods = await user_billing_dal.list_user_payment_methods(
|
||||||
@@ -552,6 +580,7 @@ async def pay_yk_saved_list_handler(callback: types.CallbackQuery, settings: Set
|
|||||||
current_lang,
|
current_lang,
|
||||||
i18n,
|
i18n,
|
||||||
has_saved_cards=False,
|
has_saved_cards=False,
|
||||||
|
sale_mode=sale_mode,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
except Exception as e_edit:
|
except Exception as e_edit:
|
||||||
@@ -565,6 +594,7 @@ async def pay_yk_saved_list_handler(callback: types.CallbackQuery, settings: Set
|
|||||||
current_lang,
|
current_lang,
|
||||||
i18n,
|
i18n,
|
||||||
has_saved_cards=False,
|
has_saved_cards=False,
|
||||||
|
sale_mode=sale_mode,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -596,6 +626,7 @@ async def pay_yk_saved_list_handler(callback: types.CallbackQuery, settings: Set
|
|||||||
current_lang,
|
current_lang,
|
||||||
i18n,
|
i18n,
|
||||||
page=page,
|
page=page,
|
||||||
|
sale_mode=sale_mode,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
except Exception as e_edit:
|
except Exception as e_edit:
|
||||||
@@ -610,6 +641,7 @@ async def pay_yk_saved_list_handler(callback: types.CallbackQuery, settings: Set
|
|||||||
current_lang,
|
current_lang,
|
||||||
i18n,
|
i18n,
|
||||||
page=page,
|
page=page,
|
||||||
|
sale_mode=sale_mode,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -633,14 +665,6 @@ async def pay_yk_use_saved_handler(callback: types.CallbackQuery, settings: Sett
|
|||||||
pass
|
pass
|
||||||
return
|
return
|
||||||
|
|
||||||
autopay_enabled = bool(settings.yookassa_autopayments_active)
|
|
||||||
if not autopay_enabled:
|
|
||||||
try:
|
|
||||||
await callback.answer(get_text("error_try_again"), show_alert=True)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return
|
|
||||||
|
|
||||||
if not yookassa_service or not yookassa_service.configured:
|
if not yookassa_service or not yookassa_service.configured:
|
||||||
logging.error("YooKassa service unavailable for pay_yk_use_saved.")
|
logging.error("YooKassa service unavailable for pay_yk_use_saved.")
|
||||||
try:
|
try:
|
||||||
@@ -673,8 +697,9 @@ async def pay_yk_use_saved_handler(callback: types.CallbackQuery, settings: Sett
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
months = int(parts[0])
|
months = float(parts[0])
|
||||||
price_rub = float(parts[1])
|
price_rub = float(parts[1])
|
||||||
|
sale_mode = parts[3] if len(parts) > 3 else "subscription"
|
||||||
except (ValueError, IndexError):
|
except (ValueError, IndexError):
|
||||||
logging.error(f"pay_yk_use_saved months/price parsing error: {callback.data}")
|
logging.error(f"pay_yk_use_saved months/price parsing error: {callback.data}")
|
||||||
try:
|
try:
|
||||||
@@ -683,6 +708,14 @@ async def pay_yk_use_saved_handler(callback: types.CallbackQuery, settings: Sett
|
|||||||
pass
|
pass
|
||||||
return
|
return
|
||||||
|
|
||||||
|
autopay_enabled = bool(settings.yookassa_autopayments_active and sale_mode != "traffic" and not settings.traffic_sale_mode)
|
||||||
|
if not autopay_enabled:
|
||||||
|
try:
|
||||||
|
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
|
||||||
method_identifier = parts[2]
|
method_identifier = parts[2]
|
||||||
user_id = callback.from_user.id
|
user_id = callback.from_user.id
|
||||||
|
|
||||||
@@ -727,9 +760,10 @@ async def pay_yk_use_saved_handler(callback: types.CallbackQuery, settings: Sett
|
|||||||
price_rub=price_rub,
|
price_rub=price_rub,
|
||||||
currency_code_for_yk=currency_code_for_yk,
|
currency_code_for_yk=currency_code_for_yk,
|
||||||
save_payment_method=False,
|
save_payment_method=False,
|
||||||
back_callback=f"pay_yk_saved_list:{months}:{price_rub}",
|
back_callback=f"pay_yk_saved_list:{_format_value(months)}:{price_rub}:{sale_mode}",
|
||||||
payment_method_id=selected_method.provider_payment_method_id,
|
payment_method_id=selected_method.provider_payment_method_id,
|
||||||
selected_method_internal_id=selected_method.method_id,
|
selected_method_internal_id=selected_method.method_id,
|
||||||
|
sale_mode=sale_mode,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|||||||
@@ -91,19 +91,31 @@ def get_trial_confirmation_keyboard(lang: str,
|
|||||||
|
|
||||||
|
|
||||||
def get_subscription_options_keyboard(subscription_options: Dict[
|
def get_subscription_options_keyboard(subscription_options: Dict[
|
||||||
int, Optional[int]], currency_symbol_val: str, lang: str,
|
float, Optional[float]], currency_symbol_val: str, lang: str,
|
||||||
i18n_instance) -> InlineKeyboardMarkup:
|
i18n_instance, traffic_mode: bool = False) -> InlineKeyboardMarkup:
|
||||||
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
|
def _format_gb(val: float) -> str:
|
||||||
|
return str(int(val)) if float(val).is_integer() else f"{val:g}"
|
||||||
if subscription_options:
|
if subscription_options:
|
||||||
for months, price in subscription_options.items():
|
for months, price in subscription_options.items():
|
||||||
if price is not None:
|
if price is not None:
|
||||||
button_text = _("subscribe_for_months_button",
|
if traffic_mode:
|
||||||
months=months,
|
button_text = _(
|
||||||
price=price,
|
"buy_traffic_package_button",
|
||||||
currency_symbol=currency_symbol_val)
|
traffic_gb=_format_gb(months),
|
||||||
|
price=price,
|
||||||
|
currency_symbol=currency_symbol_val,
|
||||||
|
)
|
||||||
|
callback_data = f"subscribe_period:{_format_gb(months)}"
|
||||||
|
else:
|
||||||
|
button_text = _("subscribe_for_months_button",
|
||||||
|
months=months,
|
||||||
|
price=price,
|
||||||
|
currency_symbol=currency_symbol_val)
|
||||||
|
callback_data = f"subscribe_period:{months}"
|
||||||
builder.button(text=button_text,
|
builder.button(text=button_text,
|
||||||
callback_data=f"subscribe_period:{months}")
|
callback_data=callback_data)
|
||||||
builder.adjust(1)
|
builder.adjust(1)
|
||||||
builder.row(
|
builder.row(
|
||||||
InlineKeyboardButton(text=_(key="back_to_main_menu_button"),
|
InlineKeyboardButton(text=_(key="back_to_main_menu_button"),
|
||||||
@@ -114,39 +126,43 @@ def get_subscription_options_keyboard(subscription_options: Dict[
|
|||||||
def get_payment_method_keyboard(months: int, price: float,
|
def get_payment_method_keyboard(months: int, price: float,
|
||||||
stars_price: Optional[int],
|
stars_price: Optional[int],
|
||||||
currency_symbol_val: str, lang: str,
|
currency_symbol_val: str, lang: str,
|
||||||
i18n_instance, settings: Settings) -> InlineKeyboardMarkup:
|
i18n_instance, settings: Settings, sale_mode: str = "subscription") -> InlineKeyboardMarkup:
|
||||||
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
|
def _format_value(val: float) -> str:
|
||||||
|
return str(int(val)) if float(val).is_integer() else f"{val:g}"
|
||||||
|
value_str = _format_value(months)
|
||||||
|
mode_suffix = f":{sale_mode}"
|
||||||
for method in settings.payment_methods_order:
|
for method in settings.payment_methods_order:
|
||||||
if method == "severpay" and getattr(settings, "SEVERPAY_ENABLED", False):
|
if method == "severpay" and getattr(settings, "SEVERPAY_ENABLED", False):
|
||||||
builder.button(
|
builder.button(
|
||||||
text=_("pay_with_severpay_button"),
|
text=_("pay_with_severpay_button"),
|
||||||
callback_data=f"pay_severpay:{months}:{price}",
|
callback_data=f"pay_severpay:{value_str}:{price}{mode_suffix}",
|
||||||
)
|
)
|
||||||
elif method == "freekassa" and settings.FREEKASSA_ENABLED:
|
elif method == "freekassa" and settings.FREEKASSA_ENABLED:
|
||||||
builder.button(
|
builder.button(
|
||||||
text=_("pay_with_sbp_button"),
|
text=_("pay_with_sbp_button"),
|
||||||
callback_data=f"pay_fk:{months}:{price}",
|
callback_data=f"pay_fk:{value_str}:{price}{mode_suffix}",
|
||||||
)
|
)
|
||||||
elif method == "platega" and settings.PLATEGA_ENABLED:
|
elif method == "platega" and settings.PLATEGA_ENABLED:
|
||||||
builder.button(
|
builder.button(
|
||||||
text=_("pay_with_platega_button"),
|
text=_("pay_with_platega_button"),
|
||||||
callback_data=f"pay_platega:{months}:{price}",
|
callback_data=f"pay_platega:{value_str}:{price}{mode_suffix}",
|
||||||
)
|
)
|
||||||
elif method == "yookassa" and settings.YOOKASSA_ENABLED:
|
elif method == "yookassa" and settings.YOOKASSA_ENABLED:
|
||||||
builder.button(
|
builder.button(
|
||||||
text=_("pay_with_yookassa_button"),
|
text=_("pay_with_yookassa_button"),
|
||||||
callback_data=f"pay_yk:{months}:{price}",
|
callback_data=f"pay_yk:{value_str}:{price}{mode_suffix}",
|
||||||
)
|
)
|
||||||
elif method == "stars" and settings.STARS_ENABLED and stars_price is not None:
|
elif method == "stars" and settings.STARS_ENABLED and stars_price is not None:
|
||||||
builder.button(
|
builder.button(
|
||||||
text=_("pay_with_stars_button"),
|
text=_("pay_with_stars_button"),
|
||||||
callback_data=f"pay_stars:{months}:{stars_price}",
|
callback_data=f"pay_stars:{value_str}:{stars_price}{mode_suffix}",
|
||||||
)
|
)
|
||||||
elif method == "cryptopay" and settings.CRYPTOPAY_ENABLED:
|
elif method == "cryptopay" and settings.CRYPTOPAY_ENABLED:
|
||||||
builder.button(
|
builder.button(
|
||||||
text=_("pay_with_cryptopay_button"),
|
text=_("pay_with_cryptopay_button"),
|
||||||
callback_data=f"pay_crypto:{months}:{price}",
|
callback_data=f"pay_crypto:{value_str}:{price}{mode_suffix}",
|
||||||
)
|
)
|
||||||
builder.button(text=_(key="cancel_button"),
|
builder.button(text=_(key="cancel_button"),
|
||||||
callback_data="main_action:subscribe")
|
callback_data="main_action:subscribe")
|
||||||
@@ -178,28 +194,33 @@ def get_yk_autopay_choice_keyboard(
|
|||||||
lang: str,
|
lang: str,
|
||||||
i18n_instance,
|
i18n_instance,
|
||||||
has_saved_cards: bool = True,
|
has_saved_cards: bool = True,
|
||||||
|
sale_mode: str = "subscription",
|
||||||
) -> InlineKeyboardMarkup:
|
) -> InlineKeyboardMarkup:
|
||||||
"""Keyboard for choosing between saved card charge or new card payment when auto-renew is enabled."""
|
"""Keyboard for choosing between saved card charge or new card payment when auto-renew is enabled."""
|
||||||
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
price_str = str(price)
|
price_str = str(price)
|
||||||
|
def _format_value(val: float) -> str:
|
||||||
|
return str(int(val)) if float(val).is_integer() else f"{val:g}"
|
||||||
|
value_str = _format_value(months)
|
||||||
|
suffix = f":{sale_mode}"
|
||||||
if has_saved_cards:
|
if has_saved_cards:
|
||||||
builder.row(
|
builder.row(
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text=_(key="yookassa_autopay_pay_saved_card_button"),
|
text=_(key="yookassa_autopay_pay_saved_card_button"),
|
||||||
callback_data=f"pay_yk_saved_list:{months}:{price_str}",
|
callback_data=f"pay_yk_saved_list:{value_str}:{price_str}{suffix}",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
builder.row(
|
builder.row(
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text=_(key="yookassa_autopay_pay_new_card_button"),
|
text=_(key="yookassa_autopay_pay_new_card_button"),
|
||||||
callback_data=f"pay_yk_new:{months}:{price_str}",
|
callback_data=f"pay_yk_new:{value_str}:{price_str}{suffix}",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
builder.row(
|
builder.row(
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text=_(key="back_to_payment_methods_button"),
|
text=_(key="back_to_payment_methods_button"),
|
||||||
callback_data=f"subscribe_period:{months}",
|
callback_data=f"subscribe_period:{value_str}",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return builder.as_markup()
|
return builder.as_markup()
|
||||||
@@ -212,6 +233,7 @@ def get_yk_saved_cards_keyboard(
|
|||||||
lang: str,
|
lang: str,
|
||||||
i18n_instance,
|
i18n_instance,
|
||||||
page: int = 0,
|
page: int = 0,
|
||||||
|
sale_mode: str = "subscription",
|
||||||
) -> InlineKeyboardMarkup:
|
) -> InlineKeyboardMarkup:
|
||||||
"""Paginated keyboard for selecting a saved YooKassa card."""
|
"""Paginated keyboard for selecting a saved YooKassa card."""
|
||||||
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
||||||
@@ -221,12 +243,16 @@ def get_yk_saved_cards_keyboard(
|
|||||||
start = page * per_page
|
start = page * per_page
|
||||||
end = min(total, start + per_page)
|
end = min(total, start + per_page)
|
||||||
price_str = str(price)
|
price_str = str(price)
|
||||||
|
def _format_value(val: float) -> str:
|
||||||
|
return str(int(val)) if float(val).is_integer() else f"{val:g}"
|
||||||
|
value_str = _format_value(months)
|
||||||
|
suffix = f":{sale_mode}"
|
||||||
|
|
||||||
for method_id, title in cards[start:end]:
|
for method_id, title in cards[start:end]:
|
||||||
builder.row(
|
builder.row(
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text=title,
|
text=title,
|
||||||
callback_data=f"pay_yk_use_saved:{months}:{price_str}:{method_id}",
|
callback_data=f"pay_yk_use_saved:{value_str}:{price_str}:{method_id}{suffix}",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -235,14 +261,14 @@ def get_yk_saved_cards_keyboard(
|
|||||||
nav_buttons.append(
|
nav_buttons.append(
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text="⬅️",
|
text="⬅️",
|
||||||
callback_data=f"pay_yk_saved_list:{months}:{price_str}:{page-1}",
|
callback_data=f"pay_yk_saved_list:{value_str}:{price_str}:{page-1}{suffix}",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if end < total:
|
if end < total:
|
||||||
nav_buttons.append(
|
nav_buttons.append(
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text="➡️",
|
text="➡️",
|
||||||
callback_data=f"pay_yk_saved_list:{months}:{price_str}:{page+1}",
|
callback_data=f"pay_yk_saved_list:{value_str}:{price_str}:{page+1}{suffix}",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if nav_buttons:
|
if nav_buttons:
|
||||||
@@ -251,13 +277,13 @@ def get_yk_saved_cards_keyboard(
|
|||||||
builder.row(
|
builder.row(
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text=_(key="yookassa_autopay_pay_new_card_button"),
|
text=_(key="yookassa_autopay_pay_new_card_button"),
|
||||||
callback_data=f"pay_yk_new:{months}:{price_str}",
|
callback_data=f"pay_yk_new:{value_str}:{price_str}{suffix}",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
builder.row(
|
builder.row(
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text=_(key="back_to_autopay_method_choice_button"),
|
text=_(key="back_to_autopay_method_choice_button"),
|
||||||
callback_data=f"pay_yk:{months}:{price_str}",
|
callback_data=f"pay_yk:{value_str}:{price_str}{suffix}",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return builder.as_markup()
|
return builder.as_markup()
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ class CryptoPayService:
|
|||||||
months: int,
|
months: int,
|
||||||
amount: float,
|
amount: float,
|
||||||
description: str,
|
description: str,
|
||||||
|
sale_mode: str = "subscription",
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
if not self.configured or not self.client:
|
if not self.configured or not self.client:
|
||||||
logging.error("CryptoPayService not configured")
|
logging.error("CryptoPayService not configured")
|
||||||
@@ -78,7 +79,7 @@ class CryptoPayService:
|
|||||||
"currency": self.settings.CRYPTOPAY_ASSET,
|
"currency": self.settings.CRYPTOPAY_ASSET,
|
||||||
"status": "pending_cryptopay",
|
"status": "pending_cryptopay",
|
||||||
"description": description,
|
"description": description,
|
||||||
"subscription_duration_months": months,
|
"subscription_duration_months": int(months),
|
||||||
"provider": "cryptopay",
|
"provider": "cryptopay",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -94,6 +95,8 @@ class CryptoPayService:
|
|||||||
"user_id": str(user_id),
|
"user_id": str(user_id),
|
||||||
"subscription_months": str(months),
|
"subscription_months": str(months),
|
||||||
"payment_db_id": str(payment_record.payment_id),
|
"payment_db_id": str(payment_record.payment_id),
|
||||||
|
"sale_mode": sale_mode,
|
||||||
|
"traffic_gb": str(months) if sale_mode == "traffic" else None,
|
||||||
})
|
})
|
||||||
try:
|
try:
|
||||||
invoice = await self.client.create_invoice(
|
invoice = await self.client.create_invoice(
|
||||||
@@ -132,8 +135,10 @@ class CryptoPayService:
|
|||||||
try:
|
try:
|
||||||
meta = json.loads(invoice.payload)
|
meta = json.loads(invoice.payload)
|
||||||
user_id = int(meta["user_id"])
|
user_id = int(meta["user_id"])
|
||||||
months = int(meta["subscription_months"])
|
months = float(meta.get("subscription_months") or 0)
|
||||||
payment_db_id = int(meta["payment_db_id"])
|
payment_db_id = int(meta["payment_db_id"])
|
||||||
|
sale_mode = meta.get("sale_mode") or ("traffic" if self.settings.traffic_sale_mode else "subscription")
|
||||||
|
traffic_gb = float(meta.get("traffic_gb")) if meta.get("traffic_gb") else months
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Failed to parse CryptoPay payload: {e}")
|
logging.error(f"Failed to parse CryptoPay payload: {e}")
|
||||||
return
|
return
|
||||||
@@ -156,18 +161,22 @@ class CryptoPayService:
|
|||||||
activation = await subscription_service.activate_subscription(
|
activation = await subscription_service.activate_subscription(
|
||||||
session,
|
session,
|
||||||
user_id,
|
user_id,
|
||||||
months,
|
int(months) if sale_mode != "traffic" else 0,
|
||||||
float(invoice.amount),
|
float(invoice.amount),
|
||||||
payment_db_id,
|
payment_db_id,
|
||||||
provider="cryptopay",
|
provider="cryptopay",
|
||||||
|
sale_mode=sale_mode,
|
||||||
|
traffic_gb=traffic_gb if sale_mode == "traffic" else None,
|
||||||
)
|
)
|
||||||
referral_bonus = await referral_service.apply_referral_bonuses_for_payment(
|
referral_bonus = None
|
||||||
session,
|
if sale_mode != "traffic":
|
||||||
user_id,
|
referral_bonus = await referral_service.apply_referral_bonuses_for_payment(
|
||||||
months,
|
session,
|
||||||
current_payment_db_id=payment_db_id,
|
user_id,
|
||||||
skip_if_active_before_payment=False,
|
int(months) or 1,
|
||||||
)
|
current_payment_db_id=payment_db_id,
|
||||||
|
skip_if_active_before_payment=False,
|
||||||
|
)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await session.rollback()
|
await session.rollback()
|
||||||
@@ -186,7 +195,12 @@ class CryptoPayService:
|
|||||||
final_end = referral_bonus["referee_new_end_date"]
|
final_end = referral_bonus["referee_new_end_date"]
|
||||||
applied_days = referral_bonus.get("referee_bonus_applied_days", 0)
|
applied_days = referral_bonus.get("referee_bonus_applied_days", 0)
|
||||||
|
|
||||||
if applied_days:
|
if sale_mode == "traffic":
|
||||||
|
text = _("payment_successful_traffic_full",
|
||||||
|
traffic_gb=str(int(traffic_gb)) if float(traffic_gb).is_integer() else f"{traffic_gb:g}",
|
||||||
|
end_date=final_end.strftime('%Y-%m-%d') if final_end else "—",
|
||||||
|
config_link=config_link)
|
||||||
|
elif applied_days:
|
||||||
inviter_name_display = _("friend_placeholder")
|
inviter_name_display = _("friend_placeholder")
|
||||||
if db_user and db_user.referred_by_id:
|
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)
|
||||||
@@ -197,7 +211,7 @@ class CryptoPayService:
|
|||||||
elif inviter.username:
|
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)
|
||||||
text = _("payment_successful_with_referral_bonus_full",
|
text = _("payment_successful_with_referral_bonus_full",
|
||||||
months=months,
|
months=int(months),
|
||||||
base_end_date=activation["end_date"].strftime('%Y-%m-%d'),
|
base_end_date=activation["end_date"].strftime('%Y-%m-%d'),
|
||||||
bonus_days=applied_days,
|
bonus_days=applied_days,
|
||||||
final_end_date=final_end.strftime('%Y-%m-%d'),
|
final_end_date=final_end.strftime('%Y-%m-%d'),
|
||||||
@@ -205,8 +219,8 @@ class CryptoPayService:
|
|||||||
config_link=config_link)
|
config_link=config_link)
|
||||||
else:
|
else:
|
||||||
text = _("payment_successful_full",
|
text = _("payment_successful_full",
|
||||||
months=months,
|
months=int(months),
|
||||||
end_date=final_end.strftime('%Y-%m-%d'),
|
end_date=final_end.strftime('%Y-%m-%d') if final_end else "—",
|
||||||
config_link=config_link)
|
config_link=config_link)
|
||||||
|
|
||||||
markup = get_connect_and_main_keyboard(
|
markup = get_connect_and_main_keyboard(
|
||||||
@@ -231,7 +245,8 @@ class CryptoPayService:
|
|||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
amount=float(invoice.amount),
|
amount=float(invoice.amount),
|
||||||
currency=invoice.asset or settings.DEFAULT_CURRENCY_SYMBOL,
|
currency=invoice.asset or settings.DEFAULT_CURRENCY_SYMBOL,
|
||||||
months=months,
|
months=int(months) if sale_mode != "traffic" else 0,
|
||||||
|
traffic_gb=traffic_gb if sale_mode == "traffic" else None,
|
||||||
payment_provider="crypto_pay",
|
payment_provider="crypto_pay",
|
||||||
username=user.username if user else None
|
username=user.username if user else None
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -284,23 +284,28 @@ class FreeKassaService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
months = payment.subscription_duration_months or 1
|
months = payment.subscription_duration_months or 1
|
||||||
|
sale_mode = "traffic" if self.settings.traffic_sale_mode else "subscription"
|
||||||
|
|
||||||
activation = await self.subscription_service.activate_subscription(
|
activation = await self.subscription_service.activate_subscription(
|
||||||
session,
|
session,
|
||||||
payment.user_id,
|
payment.user_id,
|
||||||
months,
|
int(months) if sale_mode != "traffic" else 0,
|
||||||
float(payment.amount),
|
float(payment.amount),
|
||||||
payment.payment_id,
|
payment.payment_id,
|
||||||
provider="freekassa",
|
provider="freekassa",
|
||||||
|
sale_mode=sale_mode,
|
||||||
|
traffic_gb=months if sale_mode == "traffic" else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
referral_bonus = await self.referral_service.apply_referral_bonuses_for_payment(
|
referral_bonus = None
|
||||||
session,
|
if sale_mode != "traffic":
|
||||||
payment.user_id,
|
referral_bonus = await self.referral_service.apply_referral_bonuses_for_payment(
|
||||||
months,
|
session,
|
||||||
current_payment_db_id=payment.payment_id,
|
payment.user_id,
|
||||||
skip_if_active_before_payment=False,
|
int(months),
|
||||||
)
|
current_payment_db_id=payment.payment_id,
|
||||||
|
skip_if_active_before_payment=False,
|
||||||
|
)
|
||||||
|
|
||||||
await session.commit()
|
await session.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -315,6 +320,7 @@ class FreeKassaService:
|
|||||||
config_link = None
|
config_link = None
|
||||||
final_end = None
|
final_end = None
|
||||||
months = payment.subscription_duration_months or 1
|
months = payment.subscription_duration_months or 1
|
||||||
|
sale_mode = "traffic" if self.settings.traffic_sale_mode else "subscription"
|
||||||
if activation:
|
if activation:
|
||||||
config_link = activation.get("subscription_url")
|
config_link = activation.get("subscription_url")
|
||||||
final_end = activation.get("end_date")
|
final_end = activation.get("end_date")
|
||||||
@@ -334,7 +340,14 @@ class FreeKassaService:
|
|||||||
else:
|
else:
|
||||||
end_date_str = _("config_link_not_available")
|
end_date_str = _("config_link_not_available")
|
||||||
|
|
||||||
if applied_days:
|
traffic_label = str(int(months)) if float(months).is_integer() else f"{months:g}"
|
||||||
|
|
||||||
|
if sale_mode == "traffic":
|
||||||
|
text = _("payment_successful_traffic_full",
|
||||||
|
traffic_gb=traffic_label,
|
||||||
|
end_date=end_date_str if final_end else "",
|
||||||
|
config_link=config_link)
|
||||||
|
elif applied_days:
|
||||||
inviter_name_display = _("friend_placeholder")
|
inviter_name_display = _("friend_placeholder")
|
||||||
if db_user and db_user.referred_by_id:
|
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)
|
||||||
@@ -392,7 +405,8 @@ class FreeKassaService:
|
|||||||
user_id=payment.user_id,
|
user_id=payment.user_id,
|
||||||
amount=float(payment.amount),
|
amount=float(payment.amount),
|
||||||
currency=self.default_currency,
|
currency=self.default_currency,
|
||||||
months=months,
|
months=int(months) if sale_mode != "traffic" else 0,
|
||||||
|
traffic_gb=months if sale_mode == "traffic" else None,
|
||||||
payment_provider="freekassa",
|
payment_provider="freekassa",
|
||||||
username=db_user.username if db_user else None,
|
username=db_user.username if db_user else None,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -222,7 +222,8 @@ class NotificationService:
|
|||||||
|
|
||||||
async def notify_payment_received(self, user_id: int, amount: float, currency: str,
|
async def notify_payment_received(self, user_id: int, amount: float, currency: str,
|
||||||
months: int, payment_provider: str,
|
months: int, payment_provider: str,
|
||||||
username: Optional[str] = None):
|
username: Optional[str] = None,
|
||||||
|
traffic_gb: Optional[float] = None):
|
||||||
"""Send notification about successful payment"""
|
"""Send notification about successful payment"""
|
||||||
if not self.settings.LOG_PAYMENTS:
|
if not self.settings.LOG_PAYMENTS:
|
||||||
return
|
return
|
||||||
@@ -243,23 +244,42 @@ class NotificationService:
|
|||||||
"platega": "💳",
|
"platega": "💳",
|
||||||
"severpay": "💳",
|
"severpay": "💳",
|
||||||
}.get(payment_provider.lower(), "💰")
|
}.get(payment_provider.lower(), "💰")
|
||||||
|
|
||||||
message = _(
|
if traffic_gb is not None:
|
||||||
"log_payment_received",
|
traffic_label = str(int(traffic_gb)) if float(traffic_gb).is_integer() else f"{traffic_gb:g}"
|
||||||
default="{provider_emoji} <b>Получен платеж</b>\n\n"
|
message = _(
|
||||||
"👤 Пользователь: {user_display}\n"
|
"log_payment_received_traffic",
|
||||||
"💰 Сумма: <b>{amount} {currency}</b>\n"
|
default="{provider_emoji} <b>Получен платеж</b>\n\n"
|
||||||
"📅 Период: <b>{months} мес.</b>\n"
|
"👤 Пользователь: {user_display}\n"
|
||||||
"🏦 Провайдер: {payment_provider}\n"
|
"💰 Сумма: <b>{amount} {currency}</b>\n"
|
||||||
"🕐 Время: {timestamp}",
|
"🗂 Трафик: <b>{traffic_gb} GB</b>\n"
|
||||||
provider_emoji=provider_emoji,
|
"🏦 Провайдер: {payment_provider}\n"
|
||||||
user_display=user_display,
|
"🕐 Время: {timestamp}",
|
||||||
amount=amount,
|
provider_emoji=provider_emoji,
|
||||||
currency=currency,
|
user_display=user_display,
|
||||||
months=months,
|
amount=amount,
|
||||||
payment_provider=payment_provider,
|
currency=currency,
|
||||||
timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
traffic_gb=traffic_label,
|
||||||
)
|
payment_provider=payment_provider,
|
||||||
|
timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
message = _(
|
||||||
|
"log_payment_received",
|
||||||
|
default="{provider_emoji} <b>Получен платеж</b>\n\n"
|
||||||
|
"👤 Пользователь: {user_display}\n"
|
||||||
|
"💰 Сумма: <b>{amount} {currency}</b>\n"
|
||||||
|
"📅 Период: <b>{months} мес.</b>\n"
|
||||||
|
"🏦 Провайдер: {payment_provider}\n"
|
||||||
|
"🕐 Время: {timestamp}",
|
||||||
|
provider_emoji=provider_emoji,
|
||||||
|
user_display=user_display,
|
||||||
|
amount=amount,
|
||||||
|
currency=currency,
|
||||||
|
months=months,
|
||||||
|
payment_provider=payment_provider,
|
||||||
|
timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
)
|
||||||
|
|
||||||
# Send to log channel
|
# Send to log channel
|
||||||
profile_keyboard = self._build_profile_keyboard(_, user_id)
|
profile_keyboard = self._build_profile_keyboard(_, user_id)
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ class PlategaService:
|
|||||||
return web.Response(text="ok")
|
return web.Response(text="ok")
|
||||||
|
|
||||||
payment_months = payment.subscription_duration_months or 1
|
payment_months = payment.subscription_duration_months or 1
|
||||||
|
sale_mode = "traffic" if self.settings.traffic_sale_mode else "subscription"
|
||||||
|
|
||||||
if status == "CONFIRMED":
|
if status == "CONFIRMED":
|
||||||
if amount_raw is not None:
|
if amount_raw is not None:
|
||||||
@@ -184,19 +185,23 @@ class PlategaService:
|
|||||||
activation = await self.subscription_service.activate_subscription(
|
activation = await self.subscription_service.activate_subscription(
|
||||||
session,
|
session,
|
||||||
payment.user_id,
|
payment.user_id,
|
||||||
payment_months,
|
int(payment_months) if sale_mode != "traffic" else 0,
|
||||||
float(payment.amount),
|
float(payment.amount),
|
||||||
payment.payment_id,
|
payment.payment_id,
|
||||||
provider="platega",
|
provider="platega",
|
||||||
|
sale_mode=sale_mode,
|
||||||
|
traffic_gb=payment_months if sale_mode == "traffic" else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
referral_bonus = await self.referral_service.apply_referral_bonuses_for_payment(
|
referral_bonus = None
|
||||||
session,
|
if sale_mode != "traffic":
|
||||||
payment.user_id,
|
referral_bonus = await self.referral_service.apply_referral_bonuses_for_payment(
|
||||||
payment_months,
|
session,
|
||||||
current_payment_db_id=payment.payment_id,
|
payment.user_id,
|
||||||
skip_if_active_before_payment=False,
|
int(payment_months),
|
||||||
)
|
current_payment_db_id=payment.payment_id,
|
||||||
|
skip_if_active_before_payment=False,
|
||||||
|
)
|
||||||
|
|
||||||
await session.commit()
|
await session.commit()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -221,7 +226,16 @@ class PlategaService:
|
|||||||
final_end = referral_bonus["referee_new_end_date"]
|
final_end = referral_bonus["referee_new_end_date"]
|
||||||
applied_days = referral_bonus.get("referee_bonus_applied_days", 0)
|
applied_days = referral_bonus.get("referee_bonus_applied_days", 0)
|
||||||
|
|
||||||
if applied_days:
|
traffic_label = str(int(payment_months)) if float(payment_months).is_integer() else f"{payment_months:g}"
|
||||||
|
|
||||||
|
if sale_mode == "traffic":
|
||||||
|
text = _(
|
||||||
|
"payment_successful_traffic_full",
|
||||||
|
traffic_gb=traffic_label,
|
||||||
|
end_date=final_end.strftime("%Y-%m-%d") if final_end else "",
|
||||||
|
config_link=config_link,
|
||||||
|
)
|
||||||
|
elif applied_days:
|
||||||
inviter_name_display = _("friend_placeholder")
|
inviter_name_display = _("friend_placeholder")
|
||||||
if db_user and db_user.referred_by_id:
|
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)
|
||||||
@@ -281,7 +295,8 @@ class PlategaService:
|
|||||||
user_id=payment.user_id,
|
user_id=payment.user_id,
|
||||||
amount=float(payment.amount),
|
amount=float(payment.amount),
|
||||||
currency=currency,
|
currency=currency,
|
||||||
months=payment_months,
|
months=int(payment_months) if sale_mode != "traffic" else 0,
|
||||||
|
traffic_gb=payment_months if sale_mode == "traffic" else None,
|
||||||
payment_provider="platega",
|
payment_provider="platega",
|
||||||
username=db_user.username if db_user else None,
|
username=db_user.username if db_user else None,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -190,6 +190,7 @@ class SeverPayService:
|
|||||||
return web.json_response({"status": False, "msg": "payment_not_found"}, status=404)
|
return web.json_response({"status": False, "msg": "payment_not_found"}, status=404)
|
||||||
|
|
||||||
payment_months = payment.subscription_duration_months or 1
|
payment_months = payment.subscription_duration_months or 1
|
||||||
|
sale_mode = "traffic" if self.settings.traffic_sale_mode else "subscription"
|
||||||
if status == "success":
|
if status == "success":
|
||||||
try:
|
try:
|
||||||
await payment_dal.update_provider_payment_and_status(
|
await payment_dal.update_provider_payment_and_status(
|
||||||
@@ -202,19 +203,23 @@ class SeverPayService:
|
|||||||
activation = await self.subscription_service.activate_subscription(
|
activation = await self.subscription_service.activate_subscription(
|
||||||
session,
|
session,
|
||||||
payment.user_id,
|
payment.user_id,
|
||||||
payment_months,
|
int(payment_months) if sale_mode != "traffic" else 0,
|
||||||
float(payment.amount),
|
float(payment.amount),
|
||||||
payment.payment_id,
|
payment.payment_id,
|
||||||
provider="severpay",
|
provider="severpay",
|
||||||
|
sale_mode=sale_mode,
|
||||||
|
traffic_gb=payment_months if sale_mode == "traffic" else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
referral_bonus = await self.referral_service.apply_referral_bonuses_for_payment(
|
referral_bonus = None
|
||||||
session,
|
if sale_mode != "traffic":
|
||||||
payment.user_id,
|
referral_bonus = await self.referral_service.apply_referral_bonuses_for_payment(
|
||||||
payment_months,
|
session,
|
||||||
current_payment_db_id=payment.payment_id,
|
payment.user_id,
|
||||||
skip_if_active_before_payment=False,
|
int(payment_months),
|
||||||
)
|
current_payment_db_id=payment.payment_id,
|
||||||
|
skip_if_active_before_payment=False,
|
||||||
|
)
|
||||||
|
|
||||||
await session.commit()
|
await session.commit()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -239,7 +244,16 @@ class SeverPayService:
|
|||||||
final_end = referral_bonus["referee_new_end_date"]
|
final_end = referral_bonus["referee_new_end_date"]
|
||||||
applied_days = referral_bonus.get("referee_bonus_applied_days", 0)
|
applied_days = referral_bonus.get("referee_bonus_applied_days", 0)
|
||||||
|
|
||||||
if applied_days:
|
traffic_label = str(int(payment_months)) if float(payment_months).is_integer() else f"{payment_months:g}"
|
||||||
|
|
||||||
|
if sale_mode == "traffic":
|
||||||
|
text = _(
|
||||||
|
"payment_successful_traffic_full",
|
||||||
|
traffic_gb=traffic_label,
|
||||||
|
end_date=final_end.strftime("%Y-%m-%d") if final_end else "",
|
||||||
|
config_link=config_link,
|
||||||
|
)
|
||||||
|
elif applied_days:
|
||||||
inviter_name_display = _("friend_placeholder")
|
inviter_name_display = _("friend_placeholder")
|
||||||
if db_user and db_user.referred_by_id:
|
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)
|
||||||
@@ -299,7 +313,8 @@ class SeverPayService:
|
|||||||
user_id=payment.user_id,
|
user_id=payment.user_id,
|
||||||
amount=float(payment.amount),
|
amount=float(payment.amount),
|
||||||
currency=payment.currency,
|
currency=payment.currency,
|
||||||
months=payment_months,
|
months=int(payment_months) if sale_mode != "traffic" else 0,
|
||||||
|
traffic_gb=payment_months if sale_mode == "traffic" else None,
|
||||||
payment_provider="severpay",
|
payment_provider="severpay",
|
||||||
username=db_user.username if db_user else None,
|
username=db_user.username if db_user else None,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -26,14 +26,14 @@ class StarsService:
|
|||||||
self.referral_service = referral_service
|
self.referral_service = referral_service
|
||||||
|
|
||||||
async def create_invoice(self, session: AsyncSession, user_id: int, months: int,
|
async def create_invoice(self, session: AsyncSession, user_id: int, months: int,
|
||||||
stars_price: int, description: str) -> Optional[int]:
|
stars_price: int, description: str, sale_mode: str = "subscription") -> Optional[int]:
|
||||||
payment_record_data = {
|
payment_record_data = {
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"amount": float(stars_price),
|
"amount": float(stars_price),
|
||||||
"currency": "XTR",
|
"currency": "XTR",
|
||||||
"status": "pending_stars",
|
"status": "pending_stars",
|
||||||
"description": description,
|
"description": description,
|
||||||
"subscription_duration_months": months,
|
"subscription_duration_months": int(months),
|
||||||
"provider": "telegram_stars",
|
"provider": "telegram_stars",
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
@@ -46,7 +46,7 @@ class StarsService:
|
|||||||
exc_info=True)
|
exc_info=True)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
payload = f"{db_payment_record.payment_id}:{months}"
|
payload = f"{db_payment_record.payment_id}:{months}:{sale_mode}"
|
||||||
prices = [LabeledPrice(label=description, amount=stars_price)]
|
prices = [LabeledPrice(label=description, amount=stars_price)]
|
||||||
try:
|
try:
|
||||||
await self.bot.send_invoice(
|
await self.bot.send_invoice(
|
||||||
@@ -69,7 +69,8 @@ class StarsService:
|
|||||||
payment_db_id: int,
|
payment_db_id: int,
|
||||||
months: int,
|
months: int,
|
||||||
stars_amount: int,
|
stars_amount: int,
|
||||||
i18n_data: dict) -> None:
|
i18n_data: dict,
|
||||||
|
sale_mode: str = "subscription") -> None:
|
||||||
try:
|
try:
|
||||||
await payment_dal.update_provider_payment_and_status(
|
await payment_dal.update_provider_payment_and_status(
|
||||||
session, payment_db_id,
|
session, payment_db_id,
|
||||||
@@ -86,23 +87,27 @@ class StarsService:
|
|||||||
activation_details = await self.subscription_service.activate_subscription(
|
activation_details = await self.subscription_service.activate_subscription(
|
||||||
session,
|
session,
|
||||||
message.from_user.id,
|
message.from_user.id,
|
||||||
months,
|
int(months) if sale_mode != "traffic" else 0,
|
||||||
float(stars_amount),
|
float(stars_amount),
|
||||||
payment_db_id,
|
payment_db_id,
|
||||||
provider="telegram_stars",
|
provider="telegram_stars",
|
||||||
|
sale_mode=sale_mode,
|
||||||
|
traffic_gb=months if sale_mode == "traffic" 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(
|
logging.error(
|
||||||
f"Failed to activate subscription after stars payment for user {message.from_user.id}")
|
f"Failed to activate subscription after stars payment for user {message.from_user.id}")
|
||||||
return
|
return
|
||||||
|
|
||||||
referral_bonus = await self.referral_service.apply_referral_bonuses_for_payment(
|
referral_bonus = None
|
||||||
session,
|
if sale_mode != "traffic":
|
||||||
message.from_user.id,
|
referral_bonus = await self.referral_service.apply_referral_bonuses_for_payment(
|
||||||
months,
|
session,
|
||||||
current_payment_db_id=payment_db_id,
|
message.from_user.id,
|
||||||
skip_if_active_before_payment=False,
|
int(months) or 1,
|
||||||
)
|
current_payment_db_id=payment_db_id,
|
||||||
|
skip_if_active_before_payment=False,
|
||||||
|
)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
applied_days = referral_bonus.get("referee_bonus_applied_days") if referral_bonus else None
|
applied_days = referral_bonus.get("referee_bonus_applied_days") if referral_bonus else None
|
||||||
@@ -120,7 +125,14 @@ class StarsService:
|
|||||||
"config_link_not_available"
|
"config_link_not_available"
|
||||||
)
|
)
|
||||||
|
|
||||||
if applied_days:
|
if sale_mode == "traffic":
|
||||||
|
success_msg = _(
|
||||||
|
"payment_successful_traffic_full",
|
||||||
|
traffic_gb=str(int(months)) if float(months).is_integer() else f"{months:g}",
|
||||||
|
end_date=final_end.strftime('%Y-%m-%d'),
|
||||||
|
config_link=config_link,
|
||||||
|
)
|
||||||
|
elif applied_days:
|
||||||
inviter_name_display = _("friend_placeholder")
|
inviter_name_display = _("friend_placeholder")
|
||||||
db_user = await user_dal.get_user_by_id(session, message.from_user.id)
|
db_user = await user_dal.get_user_by_id(session, message.from_user.id)
|
||||||
if db_user and db_user.referred_by_id:
|
if db_user and db_user.referred_by_id:
|
||||||
@@ -170,9 +182,10 @@ class StarsService:
|
|||||||
user_id=message.from_user.id,
|
user_id=message.from_user.id,
|
||||||
amount=float(stars_amount),
|
amount=float(stars_amount),
|
||||||
currency="XTR",
|
currency="XTR",
|
||||||
months=months,
|
months=int(months) if sale_mode != "traffic" else 0,
|
||||||
payment_provider="stars",
|
payment_provider="stars",
|
||||||
username=user.username if user else None
|
username=user.username if user else None,
|
||||||
|
traffic_gb=months if sale_mode == "traffic" else None,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Failed to send stars payment notification: {e}")
|
logging.error(f"Failed to send stars payment notification: {e}")
|
||||||
|
|||||||
@@ -419,6 +419,119 @@ class SubscriptionService:
|
|||||||
"subscription_url": final_subscription_url,
|
"subscription_url": final_subscription_url,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async def _activate_traffic_package(
|
||||||
|
self,
|
||||||
|
session: AsyncSession,
|
||||||
|
user_id: int,
|
||||||
|
traffic_gb: float,
|
||||||
|
payment_amount: float,
|
||||||
|
payment_db_id: int,
|
||||||
|
provider: str = "yookassa",
|
||||||
|
) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Activate or extend a traffic-based package instead of a time-based subscription."""
|
||||||
|
db_user = await user_dal.get_user_by_id(session, user_id)
|
||||||
|
if not db_user:
|
||||||
|
logging.error("User %s not found for traffic package activation", user_id)
|
||||||
|
return None
|
||||||
|
|
||||||
|
panel_user_uuid, panel_sub_link_id, panel_short_uuid, _ = (
|
||||||
|
await self._get_or_create_panel_user_link_details(session, user_id, db_user)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not panel_user_uuid or not panel_sub_link_id:
|
||||||
|
logging.error("Failed to ensure panel linkage for user %s during traffic activation", user_id)
|
||||||
|
return None
|
||||||
|
|
||||||
|
panel_user_data = await self.panel_service.get_user_by_uuid(panel_user_uuid) or {}
|
||||||
|
traffic_info = panel_user_data.get("userTraffic") or {}
|
||||||
|
current_limit = panel_user_data.get("trafficLimitBytes")
|
||||||
|
current_used = traffic_info.get("usedTrafficBytes")
|
||||||
|
|
||||||
|
active_sub = await subscription_dal.get_active_subscription_by_user_id(
|
||||||
|
session, user_id, panel_user_uuid
|
||||||
|
)
|
||||||
|
if current_limit is None and active_sub:
|
||||||
|
current_limit = active_sub.traffic_limit_bytes
|
||||||
|
if current_used is None and active_sub:
|
||||||
|
current_used = active_sub.traffic_used_bytes
|
||||||
|
|
||||||
|
purchase_bytes = int(float(traffic_gb) * (1024**3))
|
||||||
|
new_limit = (current_limit or 0) + purchase_bytes
|
||||||
|
|
||||||
|
start_date = datetime.now(timezone.utc)
|
||||||
|
# Set a far-future expiry to satisfy panel requirements; keep the latest known expiry if it's further.
|
||||||
|
far_future = datetime(2099, 1, 1, tzinfo=timezone.utc)
|
||||||
|
final_end_date = far_future
|
||||||
|
if active_sub and active_sub.end_date and active_sub.end_date > final_end_date:
|
||||||
|
final_end_date = active_sub.end_date
|
||||||
|
|
||||||
|
await subscription_dal.deactivate_other_active_subscriptions(
|
||||||
|
session, panel_user_uuid, panel_sub_link_id
|
||||||
|
)
|
||||||
|
|
||||||
|
sub_payload = {
|
||||||
|
"user_id": user_id,
|
||||||
|
"panel_user_uuid": panel_user_uuid,
|
||||||
|
"panel_subscription_uuid": panel_sub_link_id,
|
||||||
|
"start_date": start_date,
|
||||||
|
"end_date": final_end_date,
|
||||||
|
"duration_months": 0,
|
||||||
|
"is_active": True,
|
||||||
|
"status_from_panel": "ACTIVE",
|
||||||
|
"traffic_limit_bytes": new_limit,
|
||||||
|
"traffic_used_bytes": current_used,
|
||||||
|
"provider": provider,
|
||||||
|
"skip_notifications": True,
|
||||||
|
"auto_renew_enabled": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
new_or_updated_sub = await subscription_dal.upsert_subscription(session, sub_payload)
|
||||||
|
except Exception as exc:
|
||||||
|
logging.error("Failed to upsert traffic subscription for user %s: %s", user_id, exc, exc_info=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
panel_update_payload = self._build_panel_update_payload(
|
||||||
|
panel_user_uuid=panel_user_uuid,
|
||||||
|
expire_at=final_end_date,
|
||||||
|
status="ACTIVE",
|
||||||
|
traffic_limit_bytes=new_limit,
|
||||||
|
traffic_limit_strategy="NO_RESET",
|
||||||
|
)
|
||||||
|
|
||||||
|
panel_update_payload["description"] = "\n".join(
|
||||||
|
[
|
||||||
|
(db_user.username or "") if db_user else "",
|
||||||
|
(db_user.first_name or "") if db_user else "",
|
||||||
|
(db_user.last_name or "") if db_user else "",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_panel_user = await self.panel_service.update_user_details_on_panel(
|
||||||
|
panel_user_uuid, panel_update_payload
|
||||||
|
)
|
||||||
|
if not updated_panel_user or updated_panel_user.get("error"):
|
||||||
|
logging.warning(
|
||||||
|
"Panel user details update FAILED for traffic package user %s. Response: %s",
|
||||||
|
panel_user_uuid,
|
||||||
|
updated_panel_user,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
final_subscription_url = updated_panel_user.get("subscriptionUrl")
|
||||||
|
final_panel_short_uuid = updated_panel_user.get("shortUuid", panel_short_uuid)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"subscription_id": new_or_updated_sub.subscription_id,
|
||||||
|
"end_date": final_end_date,
|
||||||
|
"is_active": True,
|
||||||
|
"panel_user_uuid": panel_user_uuid,
|
||||||
|
"panel_short_uuid": final_panel_short_uuid,
|
||||||
|
"subscription_url": final_subscription_url,
|
||||||
|
"applied_promo_bonus_days": 0,
|
||||||
|
"traffic_limit_bytes": new_limit,
|
||||||
|
}
|
||||||
|
|
||||||
async def activate_subscription(
|
async def activate_subscription(
|
||||||
self,
|
self,
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
@@ -428,8 +541,21 @@ class SubscriptionService:
|
|||||||
payment_db_id: int,
|
payment_db_id: int,
|
||||||
promo_code_id_from_payment: Optional[int] = None,
|
promo_code_id_from_payment: Optional[int] = None,
|
||||||
provider: str = "yookassa",
|
provider: str = "yookassa",
|
||||||
|
sale_mode: str = "subscription",
|
||||||
|
traffic_gb: Optional[float] = None,
|
||||||
) -> Optional[Dict[str, Any]]:
|
) -> Optional[Dict[str, Any]]:
|
||||||
|
|
||||||
|
if sale_mode == "traffic" or getattr(self.settings, "traffic_sale_mode", False):
|
||||||
|
target_gb = traffic_gb if traffic_gb is not None else float(months)
|
||||||
|
return await self._activate_traffic_package(
|
||||||
|
session=session,
|
||||||
|
user_id=user_id,
|
||||||
|
traffic_gb=target_gb,
|
||||||
|
payment_amount=payment_amount,
|
||||||
|
payment_db_id=payment_db_id,
|
||||||
|
provider=provider,
|
||||||
|
)
|
||||||
|
|
||||||
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 not db_user:
|
if not db_user:
|
||||||
logging.error(
|
logging.error(
|
||||||
@@ -447,6 +573,11 @@ class SubscriptionService:
|
|||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
months_int = int(months)
|
||||||
|
except Exception:
|
||||||
|
months_int = 1
|
||||||
|
|
||||||
current_active_sub = await subscription_dal.get_active_subscription_by_user_id(
|
current_active_sub = await subscription_dal.get_active_subscription_by_user_id(
|
||||||
session, user_id, panel_user_uuid
|
session, user_id, panel_user_uuid
|
||||||
)
|
)
|
||||||
@@ -459,7 +590,7 @@ class SubscriptionService:
|
|||||||
start_date = current_active_sub.end_date
|
start_date = current_active_sub.end_date
|
||||||
|
|
||||||
# base duration by months
|
# base duration by months
|
||||||
end_after_months = add_months(start_date, months)
|
end_after_months = add_months(start_date, months_int)
|
||||||
duration_days_total = (end_after_months - start_date).days
|
duration_days_total = (end_after_months - start_date).days
|
||||||
applied_promo_bonus_days = 0
|
applied_promo_bonus_days = 0
|
||||||
|
|
||||||
@@ -512,7 +643,7 @@ class SubscriptionService:
|
|||||||
"panel_subscription_uuid": panel_sub_link_id,
|
"panel_subscription_uuid": panel_sub_link_id,
|
||||||
"start_date": start_date,
|
"start_date": start_date,
|
||||||
"end_date": final_end_date,
|
"end_date": final_end_date,
|
||||||
"duration_months": months,
|
"duration_months": months_int,
|
||||||
"is_active": True,
|
"is_active": True,
|
||||||
"status_from_panel": "ACTIVE",
|
"status_from_panel": "ACTIVE",
|
||||||
"traffic_limit_bytes": self.settings.user_traffic_limit_bytes,
|
"traffic_limit_bytes": self.settings.user_traffic_limit_bytes,
|
||||||
@@ -823,6 +954,9 @@ class SubscriptionService:
|
|||||||
sub: Subscription,
|
sub: Subscription,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Attempt to charge user using saved payment method. Return True on initiated/handled, False on failure."""
|
"""Attempt to charge user using saved payment method. Return True on initiated/handled, False on failure."""
|
||||||
|
if getattr(self.settings, "traffic_sale_mode", False):
|
||||||
|
logging.info("Auto-renew skipped: traffic sale mode enabled")
|
||||||
|
return True
|
||||||
if not sub.auto_renew_enabled:
|
if not sub.auto_renew_enabled:
|
||||||
return True
|
return True
|
||||||
# If autopayments are disabled globally, skip charging attempts
|
# If autopayments are disabled globally, skip charging attempts
|
||||||
@@ -902,6 +1036,7 @@ class SubscriptionService:
|
|||||||
status: Optional[str] = None,
|
status: Optional[str] = None,
|
||||||
traffic_limit_bytes: Optional[int] = None,
|
traffic_limit_bytes: Optional[int] = None,
|
||||||
include_uuid: bool = True,
|
include_uuid: bool = True,
|
||||||
|
traffic_limit_strategy: Optional[str] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
payload: Dict[str, Any] = {}
|
payload: Dict[str, Any] = {}
|
||||||
if include_uuid and panel_user_uuid:
|
if include_uuid and panel_user_uuid:
|
||||||
@@ -912,7 +1047,7 @@ class SubscriptionService:
|
|||||||
payload["status"] = status
|
payload["status"] = status
|
||||||
if traffic_limit_bytes is not None:
|
if traffic_limit_bytes is not None:
|
||||||
payload["trafficLimitBytes"] = traffic_limit_bytes
|
payload["trafficLimitBytes"] = traffic_limit_bytes
|
||||||
payload["trafficLimitStrategy"] = self.settings.USER_TRAFFIC_STRATEGY
|
payload["trafficLimitStrategy"] = traffic_limit_strategy or self.settings.USER_TRAFFIC_STRATEGY
|
||||||
if self.settings.parsed_user_squad_uuids:
|
if self.settings.parsed_user_squad_uuids:
|
||||||
payload["activeInternalSquads"] = self.settings.parsed_user_squad_uuids
|
payload["activeInternalSquads"] = self.settings.parsed_user_squad_uuids
|
||||||
if self.settings.parsed_user_external_squad_uuid:
|
if self.settings.parsed_user_external_squad_uuid:
|
||||||
|
|||||||
@@ -105,6 +105,15 @@ class Settings(BaseSettings):
|
|||||||
STARS_PRICE_12_MONTHS: Optional[int] = Field(default=None)
|
STARS_PRICE_12_MONTHS: Optional[int] = Field(default=None)
|
||||||
PANEL_WEBHOOK_SECRET: Optional[str] = Field(default=None)
|
PANEL_WEBHOOK_SECRET: Optional[str] = Field(default=None)
|
||||||
|
|
||||||
|
TRAFFIC_PACKAGES: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
description="Comma-separated list of traffic packages in the format '<GB>:<price>', e.g. '10:199,50:799'",
|
||||||
|
)
|
||||||
|
STARS_TRAFFIC_PACKAGES: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
description="Comma-separated list of traffic packages priced in Stars, e.g. '5:500,20:1500'",
|
||||||
|
)
|
||||||
|
|
||||||
SUBSCRIPTION_NOTIFICATIONS_ENABLED: bool = Field(default=True)
|
SUBSCRIPTION_NOTIFICATIONS_ENABLED: bool = Field(default=True)
|
||||||
SUBSCRIPTION_NOTIFY_ON_EXPIRE: bool = Field(default=True)
|
SUBSCRIPTION_NOTIFY_ON_EXPIRE: bool = Field(default=True)
|
||||||
SUBSCRIPTION_NOTIFY_AFTER_EXPIRE: bool = Field(default=True)
|
SUBSCRIPTION_NOTIFY_AFTER_EXPIRE: bool = Field(default=True)
|
||||||
@@ -360,6 +369,62 @@ class Settings(BaseSettings):
|
|||||||
options[12] = self.STARS_PRICE_12_MONTHS
|
options[12] = self.STARS_PRICE_12_MONTHS
|
||||||
return options
|
return options
|
||||||
|
|
||||||
|
@computed_field
|
||||||
|
@property
|
||||||
|
def traffic_packages(self) -> Dict[float, float]:
|
||||||
|
"""
|
||||||
|
Mapping of traffic size in GB to price in the default currency.
|
||||||
|
"""
|
||||||
|
packages: Dict[float, float] = {}
|
||||||
|
raw = (self.TRAFFIC_PACKAGES or "").strip()
|
||||||
|
if not raw:
|
||||||
|
return packages
|
||||||
|
for part in raw.split(","):
|
||||||
|
chunk = part.strip()
|
||||||
|
if not chunk or ":" not in chunk:
|
||||||
|
continue
|
||||||
|
size_str, price_str = chunk.split(":", 1)
|
||||||
|
try:
|
||||||
|
size_gb = float(size_str.strip())
|
||||||
|
price_val = float(price_str.strip())
|
||||||
|
if size_gb > 0 and price_val >= 0:
|
||||||
|
packages[size_gb] = price_val
|
||||||
|
except ValueError:
|
||||||
|
logging.warning("Invalid TRAFFIC_PACKAGES entry skipped: %s", chunk)
|
||||||
|
continue
|
||||||
|
return packages
|
||||||
|
|
||||||
|
@computed_field
|
||||||
|
@property
|
||||||
|
def stars_traffic_packages(self) -> Dict[float, int]:
|
||||||
|
"""
|
||||||
|
Mapping of traffic size in GB to price in Telegram Stars.
|
||||||
|
"""
|
||||||
|
packages: Dict[float, int] = {}
|
||||||
|
raw = (self.STARS_TRAFFIC_PACKAGES or "").strip()
|
||||||
|
if not raw:
|
||||||
|
return packages
|
||||||
|
for part in raw.split(","):
|
||||||
|
chunk = part.strip()
|
||||||
|
if not chunk or ":" not in chunk:
|
||||||
|
continue
|
||||||
|
size_str, price_str = chunk.split(":", 1)
|
||||||
|
try:
|
||||||
|
size_gb = float(size_str.strip())
|
||||||
|
price_val = int(float(price_str.strip()))
|
||||||
|
if size_gb > 0 and price_val >= 0:
|
||||||
|
packages[size_gb] = price_val
|
||||||
|
except ValueError:
|
||||||
|
logging.warning("Invalid STARS_TRAFFIC_PACKAGES entry skipped: %s", chunk)
|
||||||
|
continue
|
||||||
|
return packages
|
||||||
|
|
||||||
|
@computed_field
|
||||||
|
@property
|
||||||
|
def traffic_sale_mode(self) -> bool:
|
||||||
|
"""When true, the bot sells traffic packages instead of time-based subscriptions."""
|
||||||
|
return bool(self.traffic_packages)
|
||||||
|
|
||||||
def referral_bonus_inviter(self) -> Dict[int, int]:
|
def referral_bonus_inviter(self) -> Dict[int, int]:
|
||||||
bonuses: Dict[int, int] = {}
|
bonuses: Dict[int, int] = {}
|
||||||
if self.REFERRAL_BONUS_DAYS_INVITER_1_MONTH is not None:
|
if self.REFERRAL_BONUS_DAYS_INVITER_1_MONTH is not None:
|
||||||
|
|||||||
@@ -25,8 +25,11 @@
|
|||||||
"error_displaying_menu": "Error displaying menu.",
|
"error_displaying_menu": "Error displaying menu.",
|
||||||
"main_menu_unknown_action": "Unknown action.",
|
"main_menu_unknown_action": "Unknown action.",
|
||||||
"select_subscription_period": "Select subscription period:",
|
"select_subscription_period": "Select subscription period:",
|
||||||
|
"select_traffic_package": "Select a traffic package:",
|
||||||
"subscribe_for_months_button": "{months} mo. - {price} {currency_symbol}",
|
"subscribe_for_months_button": "{months} mo. - {price} {currency_symbol}",
|
||||||
|
"buy_traffic_package_button": "{traffic_gb} GB - {price} {currency_symbol}",
|
||||||
"choose_payment_method": "Choose payment method:",
|
"choose_payment_method": "Choose payment method:",
|
||||||
|
"choose_payment_method_traffic": "Choose how to pay for the traffic package:",
|
||||||
"pay_button": "💳 Pay",
|
"pay_button": "💳 Pay",
|
||||||
"pay_with_yookassa_button": "💳 YooKassa",
|
"pay_with_yookassa_button": "💳 YooKassa",
|
||||||
"yookassa_autopay_flow_prompt": "Auto-renew is enabled. Choose how you'd like to pay:",
|
"yookassa_autopay_flow_prompt": "Auto-renew is enabled. Choose how you'd like to pay:",
|
||||||
@@ -56,11 +59,15 @@
|
|||||||
"my_devices_feature_disabled": "The My Devices section is currently unavailable.",
|
"my_devices_feature_disabled": "The My Devices section is currently unavailable.",
|
||||||
|
|
||||||
"payment_description_subscription": "Subscription payment for {months} mo.",
|
"payment_description_subscription": "Subscription payment for {months} mo.",
|
||||||
|
"payment_description_traffic": "Traffic package {traffic_gb} GB",
|
||||||
"payment_link_message": "To pay for {months} mo. subscription, click the button below:",
|
"payment_link_message": "To pay for {months} mo. subscription, click the button below:",
|
||||||
|
"payment_link_message_traffic": "To pay for a {traffic_gb} GB package, tap the button below:",
|
||||||
"free_kassa_order_info": "Order #{order_id} from {date}",
|
"free_kassa_order_info": "Order #{order_id} from {date}",
|
||||||
"payment_invoice_sent_message": "Telegram has sent the invoice above. Complete the payment or pick another method below.",
|
"payment_invoice_sent_message": "Telegram has sent the invoice above. Complete the payment or pick another method below.",
|
||||||
|
"payment_invoice_sent_message_traffic": "Invoice for {traffic_gb} GB sent above. Complete the payment or pick another method below.",
|
||||||
"payment_successful_error_details": "✅ Payment succeeded, but an error occurred displaying details. Your subscription is active. Contact support if anything is wrong.",
|
"payment_successful_error_details": "✅ Payment succeeded, but an error occurred displaying details. Your subscription is active. Contact support if anything is wrong.",
|
||||||
"payment_successful_full": "✅ Payment successful!\nYour {months}-month subscription is active until {end_date}.\n\nConnection key:\n<code>{config_link}</code>\n\nTo connect, open the link and follow the instructions 👇",
|
"payment_successful_full": "✅ Payment successful!\nYour {months}-month subscription is active until {end_date}.\n\nConnection key:\n<code>{config_link}</code>\n\nTo connect, open the link and follow the instructions 👇",
|
||||||
|
"payment_successful_traffic_full": "✅ Payment successful!\nYour {traffic_gb} GB package is active.\nValidity: {end_date}\n\nConnection key:\n<code>{config_link}</code>\n\nTo connect, open the link and follow the instructions 👇",
|
||||||
"payment_successful_with_referral_bonus_full": "✅ Payment successful!\nYour {months}-month subscription (base end date: {base_end_date}) has been extended by {bonus_days} bonus days for referral from {inviter_name} and is now active until {final_end_date}.\n\nConnection key:\n<code>{config_link}</code>\n\nTo connect, open the link and follow the instructions 👇",
|
"payment_successful_with_referral_bonus_full": "✅ Payment successful!\nYour {months}-month subscription (base end date: {base_end_date}) has been extended by {bonus_days} bonus days for referral from {inviter_name} and is now active until {final_end_date}.\n\nConnection key:\n<code>{config_link}</code>\n\nTo connect, open the link and follow the instructions 👇",
|
||||||
"payment_failed": "❌ Payment failed or was cancelled. Please try again or contact support.",
|
"payment_failed": "❌ Payment failed or was cancelled. Please try again or contact support.",
|
||||||
"config_link_not_available": "not available, contact support",
|
"config_link_not_available": "not available, contact support",
|
||||||
@@ -80,6 +87,7 @@
|
|||||||
"no_button": "No",
|
"no_button": "No",
|
||||||
"referral_program_info_new": "🎁 <b>Referral Program</b>\n\n📊 <b>Your stats:</b>\n👥 Friends invited: <b>{invited_count}</b>\n💳 Purchased subscription: <b>{purchased_count}</b>\n\n🔗 Your link:\n<code>{referral_link}</code>\n\n💰 <b>Invitation bonuses:</b>\n{bonus_details}\n\n📢 Share the link with friends and get bonuses!",
|
"referral_program_info_new": "🎁 <b>Referral Program</b>\n\n📊 <b>Your stats:</b>\n👥 Friends invited: <b>{invited_count}</b>\n💳 Purchased subscription: <b>{purchased_count}</b>\n\n🔗 Your link:\n<code>{referral_link}</code>\n\n💰 <b>Invitation bonuses:</b>\n{bonus_details}\n\n📢 Share the link with friends and get bonuses!",
|
||||||
"referral_bonus_per_period": "\n\n🎁 For a friend's {months}-month subscription:\n ➢ You: <b>{inviter_bonus_days} days</b>\n ➢ Friend: <b>{referee_bonus_days} days</b>",
|
"referral_bonus_per_period": "\n\n🎁 For a friend's {months}-month subscription:\n ➢ You: <b>{inviter_bonus_days} days</b>\n ➢ Friend: <b>{referee_bonus_days} days</b>",
|
||||||
|
"referral_not_available_for_traffic": "Referral bonuses are not available for traffic packages.",
|
||||||
"referral_share_message_button": "📩 Message for friend",
|
"referral_share_message_button": "📩 Message for friend",
|
||||||
"referral_friend_message": "🚀 Hey! Try this VPN - it's fast, reliable and affordable!\n\n🎁 Use my link to get bonus days with your subscription!\n\n{referral_link}",
|
"referral_friend_message": "🚀 Hey! Try this VPN - it's fast, reliable and affordable!\n\n🎁 Use my link to get bonus days with your subscription!\n\n{referral_link}",
|
||||||
"friend_placeholder": "friend",
|
"friend_placeholder": "friend",
|
||||||
@@ -131,6 +139,7 @@
|
|||||||
"admin_csv_provider": "Provider",
|
"admin_csv_provider": "Provider",
|
||||||
"admin_csv_status": "Status",
|
"admin_csv_status": "Status",
|
||||||
"admin_csv_description": "Description",
|
"admin_csv_description": "Description",
|
||||||
|
"admin_csv_units": "Months/GB",
|
||||||
"admin_csv_months": "Months",
|
"admin_csv_months": "Months",
|
||||||
"admin_csv_created_at": "Created At",
|
"admin_csv_created_at": "Created At",
|
||||||
"admin_csv_provider_payment_id": "Provider Payment ID",
|
"admin_csv_provider_payment_id": "Provider Payment ID",
|
||||||
@@ -297,6 +306,7 @@
|
|||||||
"log_open_referrer_profile_button": "👤 Referrer profile",
|
"log_open_referrer_profile_button": "👤 Referrer profile",
|
||||||
"log_new_user_registration": "👤 <b>New User</b>\n\n🆔 ID: <code>{user_id}</code>\n👤 Name: {user_display}{referral_text}\n📅 Time: {timestamp}",
|
"log_new_user_registration": "👤 <b>New User</b>\n\n🆔 ID: <code>{user_id}</code>\n👤 Name: {user_display}{referral_text}\n📅 Time: {timestamp}",
|
||||||
"log_payment_received": "{provider_emoji} <b>Payment Received</b>\n\n👤 User: {user_display}\n💰 Amount: <b>{amount} {currency}</b>\n📅 Period: <b>{months} mo.</b>\n🏦 Provider: {payment_provider}\n🕐 Time: {timestamp}",
|
"log_payment_received": "{provider_emoji} <b>Payment Received</b>\n\n👤 User: {user_display}\n💰 Amount: <b>{amount} {currency}</b>\n📅 Period: <b>{months} mo.</b>\n🏦 Provider: {payment_provider}\n🕐 Time: {timestamp}",
|
||||||
|
"log_payment_received_traffic": "{provider_emoji} <b>Payment Received</b>\n\n👤 User: {user_display}\n💰 Amount: <b>{amount} {currency}</b>\n🗂 Traffic: <b>{traffic_gb} GB</b>\n🏦 Provider: {payment_provider}\n🕐 Time: {timestamp}",
|
||||||
"log_promo_activation": "🎁 <b>Promo Code Activated</b>\n\n👤 User: {user_display}\n🏷 Code: <code>{promo_code}</code>\n🎯 Bonus: <b>+{bonus_days}d</b>\n🕐 Time: {timestamp}",
|
"log_promo_activation": "🎁 <b>Promo Code Activated</b>\n\n👤 User: {user_display}\n🏷 Code: <code>{promo_code}</code>\n🎯 Bonus: <b>+{bonus_days}d</b>\n🕐 Time: {timestamp}",
|
||||||
"log_trial_activation": "🆓 <b>Trial Activated</b>\n\n👤 User: {user_display}\n⏰ Valid until: <b>{end_date}</b>\n🕐 Time: {timestamp}",
|
"log_trial_activation": "🆓 <b>Trial Activated</b>\n\n👤 User: {user_display}\n⏰ Valid until: <b>{end_date}</b>\n🕐 Time: {timestamp}",
|
||||||
"log_panel_sync": "{status_emoji} <b>Panel Synchronization</b>\n\n📊 Status: <b>{status}</b>\n👥 Users processed: <b>{users_processed}</b>\n📋 Subscriptions synced: <b>{subs_synced}</b>\n🕐 Time: {timestamp}\n\n📝 Details:\n{details}",
|
"log_panel_sync": "{status_emoji} <b>Panel Synchronization</b>\n\n📊 Status: <b>{status}</b>\n👥 Users processed: <b>{users_processed}</b>\n📋 Subscriptions synced: <b>{subs_synced}</b>\n🕐 Time: {timestamp}\n\n📝 Details:\n{details}",
|
||||||
@@ -416,7 +426,11 @@
|
|||||||
"admin_sync_no_telegram_id": "\n⚠️ Records without telegramId: {count}",
|
"admin_sync_no_telegram_id": "\n⚠️ Records without telegramId: {count}",
|
||||||
"admin_sync_not_found_in_db": "\n❌ Not found in DB: {count}",
|
"admin_sync_not_found_in_db": "\n❌ Not found in DB: {count}",
|
||||||
"admin_payments_pagination_info": "📊 Showing {shown} of {total} payments (page {current_page}/{total_pages})",
|
"admin_payments_pagination_info": "📊 Showing {shown} of {total} payments (page {current_page}/{total_pages})",
|
||||||
|
"admin_payment_traffic_label": "🗂 Traffic: <b>{traffic_gb} GB</b>",
|
||||||
|
"admin_payment_months_label": "📅 Period: <b>{months} mo.</b>",
|
||||||
"my_subscription_details": "🔐 <b>My Subscription</b>\n\n⏰ Status: <b>{status}</b>\n📅 Active until: <b>{end_date}</b>\n📆 Days left: <b>{days_left}</b>\n\n🔗 Configuration link:\n<code>{config_link}</code>\n\n📊 Traffic:\nLimit: <b>{traffic_limit}</b>\nUsed: <b>{traffic_used}</b>",
|
"my_subscription_details": "🔐 <b>My Subscription</b>\n\n⏰ Status: <b>{status}</b>\n📅 Active until: <b>{end_date}</b>\n📆 Days left: <b>{days_left}</b>\n\n🔗 Configuration link:\n<code>{config_link}</code>\n\n📊 Traffic:\nLimit: <b>{traffic_limit}</b>\nUsed: <b>{traffic_used}</b>",
|
||||||
|
"my_traffic_details": "🔐 <b>My Traffic</b>\n\n⏰ Status: <b>{status}</b>\n📅 Valid until: <b>{end_date}</b>\n\n🔗 Configuration link:\n<code>{config_link}</code>\n\n📊 Traffic:\nLimit: <b>{traffic_limit}</b>\nUsed: <b>{traffic_used}</b>\nLeft: <b>{traffic_left}</b>",
|
||||||
|
"traffic_no_expiry": "no limit",
|
||||||
"autorenew_enable_button": "🔄 Enable auto-renew",
|
"autorenew_enable_button": "🔄 Enable auto-renew",
|
||||||
"autorenew_disable_button": "🛑 Disable auto-renew",
|
"autorenew_disable_button": "🛑 Disable auto-renew",
|
||||||
"subscription_autorenew_updated": "Auto-renew settings updated.",
|
"subscription_autorenew_updated": "Auto-renew settings updated.",
|
||||||
@@ -439,6 +453,7 @@
|
|||||||
"payment_method_tx_history_title": "📜 Transactions history",
|
"payment_method_tx_history_title": "📜 Transactions history",
|
||||||
"payment_method_no_history": "No transactions history.",
|
"payment_method_no_history": "No transactions history.",
|
||||||
"subscription_purchase_title": "Subscription purchase for {months} mo.",
|
"subscription_purchase_title": "Subscription purchase for {months} mo.",
|
||||||
|
"traffic_purchase_title": "Traffic purchase {traffic_gb} GB",
|
||||||
"autorenew_enable_requires_card": "Link a payment card in Payment Methods before enabling auto-renew.",
|
"autorenew_enable_requires_card": "Link a payment card in Payment Methods before enabling auto-renew.",
|
||||||
"subscription_not_active": "You don't have an active subscription.",
|
"subscription_not_active": "You don't have an active subscription.",
|
||||||
"error_service_unavailable": "Service unavailable. Please try again later.",
|
"error_service_unavailable": "Service unavailable. Please try again later.",
|
||||||
|
|||||||
@@ -25,8 +25,11 @@
|
|||||||
"error_displaying_menu": "Ошибка отображения меню.",
|
"error_displaying_menu": "Ошибка отображения меню.",
|
||||||
"main_menu_unknown_action": "Неизвестное действие.",
|
"main_menu_unknown_action": "Неизвестное действие.",
|
||||||
"select_subscription_period": "Выберите срок подписки:",
|
"select_subscription_period": "Выберите срок подписки:",
|
||||||
|
"select_traffic_package": "Выберите пакет трафика:",
|
||||||
"subscribe_for_months_button": "{months} мес. - {price} {currency_symbol}",
|
"subscribe_for_months_button": "{months} мес. - {price} {currency_symbol}",
|
||||||
|
"buy_traffic_package_button": "{traffic_gb} ГБ - {price} {currency_symbol}",
|
||||||
"choose_payment_method": "Выберите способ оплаты:",
|
"choose_payment_method": "Выберите способ оплаты:",
|
||||||
|
"choose_payment_method_traffic": "Выберите способ оплаты пакета трафика:",
|
||||||
"pay_button": "💳 Оплатить",
|
"pay_button": "💳 Оплатить",
|
||||||
"pay_with_yookassa_button": "💳 ЮKassa",
|
"pay_with_yookassa_button": "💳 ЮKassa",
|
||||||
"yookassa_autopay_flow_prompt": "Автопродление включено. Выберите, как оплатить подписку:",
|
"yookassa_autopay_flow_prompt": "Автопродление включено. Выберите, как оплатить подписку:",
|
||||||
@@ -56,11 +59,15 @@
|
|||||||
|
|
||||||
"cancel_button": "❌ Отмена",
|
"cancel_button": "❌ Отмена",
|
||||||
"payment_description_subscription": "Оплата подписки на {months} мес.",
|
"payment_description_subscription": "Оплата подписки на {months} мес.",
|
||||||
|
"payment_description_traffic": "Пакет трафика {traffic_gb} ГБ",
|
||||||
"payment_link_message": "Для оплаты подписки на {months} мес., нажмите кнопку ниже:",
|
"payment_link_message": "Для оплаты подписки на {months} мес., нажмите кнопку ниже:",
|
||||||
|
"payment_link_message_traffic": "Для оплаты пакета {traffic_gb} ГБ нажмите кнопку ниже:",
|
||||||
"free_kassa_order_info": "Заказ №{order_id} от {date}",
|
"free_kassa_order_info": "Заказ №{order_id} от {date}",
|
||||||
"payment_invoice_sent_message": "Счёт Telegram Stars отправлен выше. Нажмите «Оплатить» или вернитесь к выбору способа ниже.",
|
"payment_invoice_sent_message": "Счёт Telegram Stars отправлен выше. Нажмите «Оплатить» или вернитесь к выбору способа ниже.",
|
||||||
|
"payment_invoice_sent_message_traffic": "Счет на пакет {traffic_gb} ГБ отправлен выше. Нажмите «Оплатить» или вернитесь к выбору способа ниже.",
|
||||||
"payment_successful_error_details": "✅ Оплата прошла успешно, но возникла ошибка при отображении деталей. Ваша подписка активна. Свяжитесь с поддержкой, если что-то не так.",
|
"payment_successful_error_details": "✅ Оплата прошла успешно, но возникла ошибка при отображении деталей. Ваша подписка активна. Свяжитесь с поддержкой, если что-то не так.",
|
||||||
"payment_successful_full": "✅ Оплата прошла успешно!\nВаша подписка на {months} мес. активна до {end_date}.\n\nКлюч подключения:\n<code>{config_link}</code>\n\nЧтобы подключиться, перейдите по ссылке и следуйте инструкции 👇",
|
"payment_successful_full": "✅ Оплата прошла успешно!\nВаша подписка на {months} мес. активна до {end_date}.\n\nКлюч подключения:\n<code>{config_link}</code>\n\nЧтобы подключиться, перейдите по ссылке и следуйте инструкции 👇",
|
||||||
|
"payment_successful_traffic_full": "✅ Оплата прошла успешно!\nВаш пакет {traffic_gb} ГБ активирован.\nДата действия: {end_date}\n\nКлюч подключения:\n<code>{config_link}</code>\n\nЧтобы подключиться, перейдите по ссылке и следуйте инструкции 👇",
|
||||||
"payment_successful_with_referral_bonus_full": "✅ Оплата прошла успешно!\nВаша подписка на {months} мес. (базовая дата окончания: {base_end_date}) продлена на {bonus_days} бонусных дней за приглашение от {inviter_name} и теперь активна до {final_end_date}.\n\nКлюч подключения:\n<code>{config_link}</code>\n\nЧтобы подключиться, перейдите по ссылке и следуйте инструкции 👇",
|
"payment_successful_with_referral_bonus_full": "✅ Оплата прошла успешно!\nВаша подписка на {months} мес. (базовая дата окончания: {base_end_date}) продлена на {bonus_days} бонусных дней за приглашение от {inviter_name} и теперь активна до {final_end_date}.\n\nКлюч подключения:\n<code>{config_link}</code>\n\nЧтобы подключиться, перейдите по ссылке и следуйте инструкции 👇",
|
||||||
"payment_failed": "❌ Оплата не удалась или была отменена. Пожалуйста, попробуйте еще раз или свяжитесь с поддержкой.",
|
"payment_failed": "❌ Оплата не удалась или была отменена. Пожалуйста, попробуйте еще раз или свяжитесь с поддержкой.",
|
||||||
"config_link_not_available": "недоступна, обратитесь в поддержку",
|
"config_link_not_available": "недоступна, обратитесь в поддержку",
|
||||||
@@ -80,6 +87,7 @@
|
|||||||
"no_button": "Нет",
|
"no_button": "Нет",
|
||||||
"referral_program_info_new": "🎁 <b>Реферальная программа</b>\n\n📊 <b>Твоя статистика:</b>\n👥 Приглашено друзей: <b>{invited_count}</b>\n💳 Купили подписку: <b>{purchased_count}</b>\n\n🔗 Твоя ссылка:\n<code>{referral_link}</code>\n\n💰 <b>Бонусы за приглашения:</b>\n{bonus_details}\n\n📢 Поделись ссылкой с друзьями и получай бонусы!",
|
"referral_program_info_new": "🎁 <b>Реферальная программа</b>\n\n📊 <b>Твоя статистика:</b>\n👥 Приглашено друзей: <b>{invited_count}</b>\n💳 Купили подписку: <b>{purchased_count}</b>\n\n🔗 Твоя ссылка:\n<code>{referral_link}</code>\n\n💰 <b>Бонусы за приглашения:</b>\n{bonus_details}\n\n📢 Поделись ссылкой с друзьями и получай бонусы!",
|
||||||
"referral_bonus_per_period": "\n\n🎁 За {months}-мес. подписку друга:\n ➢ Вы: <b>{inviter_bonus_days} дн.</b>\n ➢ Друг: <b>{referee_bonus_days} дн.</b>",
|
"referral_bonus_per_period": "\n\n🎁 За {months}-мес. подписку друга:\n ➢ Вы: <b>{inviter_bonus_days} дн.</b>\n ➢ Друг: <b>{referee_bonus_days} дн.</b>",
|
||||||
|
"referral_not_available_for_traffic": "Для пакетов трафика реферальные бонусы не начисляются.",
|
||||||
"referral_share_message_button": "📩 Сообщение для друга",
|
"referral_share_message_button": "📩 Сообщение для друга",
|
||||||
"referral_friend_message": "🚀 Привет! Попробуй этот VPN - быстрый, надёжный и доступный!\n\n🎁 По моей ссылке тебе дадут бонусные дни к подписке!\n\n{referral_link}",
|
"referral_friend_message": "🚀 Привет! Попробуй этот VPN - быстрый, надёжный и доступный!\n\n🎁 По моей ссылке тебе дадут бонусные дни к подписке!\n\n{referral_link}",
|
||||||
"friend_placeholder": "друг",
|
"friend_placeholder": "друг",
|
||||||
@@ -131,6 +139,7 @@
|
|||||||
"admin_csv_provider": "Платежная система",
|
"admin_csv_provider": "Платежная система",
|
||||||
"admin_csv_status": "Статус",
|
"admin_csv_status": "Статус",
|
||||||
"admin_csv_description": "Описание",
|
"admin_csv_description": "Описание",
|
||||||
|
"admin_csv_units": "Месяцы/ГБ",
|
||||||
"admin_csv_months": "Месяцев",
|
"admin_csv_months": "Месяцев",
|
||||||
"admin_csv_created_at": "Дата создания",
|
"admin_csv_created_at": "Дата создания",
|
||||||
"admin_csv_provider_payment_id": "ID платежа в системе",
|
"admin_csv_provider_payment_id": "ID платежа в системе",
|
||||||
@@ -297,6 +306,7 @@
|
|||||||
"log_open_referrer_profile_button": "👤 Профиль пригласившего",
|
"log_open_referrer_profile_button": "👤 Профиль пригласившего",
|
||||||
"log_new_user_registration": "👤 <b>Новый пользователь</b>\n\n🆔 ID: <code>{user_id}</code>\n👤 Имя: {user_display}{referral_text}\n📅 Время: {timestamp}",
|
"log_new_user_registration": "👤 <b>Новый пользователь</b>\n\n🆔 ID: <code>{user_id}</code>\n👤 Имя: {user_display}{referral_text}\n📅 Время: {timestamp}",
|
||||||
"log_payment_received": "{provider_emoji} <b>Получен платеж</b>\n\n👤 Пользователь: {user_display}\n💰 Сумма: <b>{amount} {currency}</b>\n📅 Период: <b>{months} мес.</b>\n🏦 Провайдер: {payment_provider}\n🕐 Время: {timestamp}",
|
"log_payment_received": "{provider_emoji} <b>Получен платеж</b>\n\n👤 Пользователь: {user_display}\n💰 Сумма: <b>{amount} {currency}</b>\n📅 Период: <b>{months} мес.</b>\n🏦 Провайдер: {payment_provider}\n🕐 Время: {timestamp}",
|
||||||
|
"log_payment_received_traffic": "{provider_emoji} <b>Получен платеж</b>\n\n👤 Пользователь: {user_display}\n💰 Сумма: <b>{amount} {currency}</b>\n🗂 Трафик: <b>{traffic_gb} ГБ</b>\n🏦 Провайдер: {payment_provider}\n🕐 Время: {timestamp}",
|
||||||
"log_promo_activation": "🎁 <b>Активирован промокод</b>\n\n👤 Пользователь: {user_display}\n🏷 Код: <code>{promo_code}</code>\n🎯 Бонус: <b>+{bonus_days} дн.</b>\n🕐 Время: {timestamp}",
|
"log_promo_activation": "🎁 <b>Активирован промокод</b>\n\n👤 Пользователь: {user_display}\n🏷 Код: <code>{promo_code}</code>\n🎯 Бонус: <b>+{bonus_days} дн.</b>\n🕐 Время: {timestamp}",
|
||||||
"log_trial_activation": "🆓 <b>Активирован триал</b>\n\n👤 Пользователь: {user_display}\n⏰ Действует до: <b>{end_date}</b>\n🕐 Время: {timestamp}",
|
"log_trial_activation": "🆓 <b>Активирован триал</b>\n\n👤 Пользователь: {user_display}\n⏰ Действует до: <b>{end_date}</b>\n🕐 Время: {timestamp}",
|
||||||
"log_panel_sync": "{status_emoji} <b>Синхронизация с панелью</b>\n\n📊 Статус: <b>{status}</b>\n👥 Обработано пользователей: <b>{users_processed}</b>\n📋 Синхронизировано подписок: <b>{subs_synced}</b>\n🕐 Время: {timestamp}\n\n📝 Детали:\n{details}",
|
"log_panel_sync": "{status_emoji} <b>Синхронизация с панелью</b>\n\n📊 Статус: <b>{status}</b>\n👥 Обработано пользователей: <b>{users_processed}</b>\n📋 Синхронизировано подписок: <b>{subs_synced}</b>\n🕐 Время: {timestamp}\n\n📝 Детали:\n{details}",
|
||||||
@@ -416,7 +426,11 @@
|
|||||||
"admin_sync_no_telegram_id": "\n⚠️ Записей без telegramId: {count}",
|
"admin_sync_no_telegram_id": "\n⚠️ Записей без telegramId: {count}",
|
||||||
"admin_sync_not_found_in_db": "\n❌ Не найдено в БД: {count}",
|
"admin_sync_not_found_in_db": "\n❌ Не найдено в БД: {count}",
|
||||||
"admin_payments_pagination_info": "📊 Показано {shown} из {total} платежей (стр. {current_page}/{total_pages})",
|
"admin_payments_pagination_info": "📊 Показано {shown} из {total} платежей (стр. {current_page}/{total_pages})",
|
||||||
|
"admin_payment_traffic_label": "🗂 Трафик: <b>{traffic_gb} ГБ</b>",
|
||||||
|
"admin_payment_months_label": "📅 Период: <b>{months} мес.</b>",
|
||||||
"my_subscription_details": "🔐 <b>Моя подписка</b>\n\n⏰ Статус: <b>{status}</b>\n📅 Действует до: <b>{end_date}</b>\n📆 Осталось дней: <b>{days_left}</b>\n\n🔗 Ссылка на конфигурацию:\n<code>{config_link}</code>\n\n📊 Трафик:\nЛимит: <b>{traffic_limit}</b>\nИспользовано: <b>{traffic_used}</b>",
|
"my_subscription_details": "🔐 <b>Моя подписка</b>\n\n⏰ Статус: <b>{status}</b>\n📅 Действует до: <b>{end_date}</b>\n📆 Осталось дней: <b>{days_left}</b>\n\n🔗 Ссылка на конфигурацию:\n<code>{config_link}</code>\n\n📊 Трафик:\nЛимит: <b>{traffic_limit}</b>\nИспользовано: <b>{traffic_used}</b>",
|
||||||
|
"my_traffic_details": "🔐 <b>Мой трафик</b>\n\n⏰ Статус: <b>{status}</b>\n📅 Действует до: <b>{end_date}</b>\n\n🔗 Ссылка на конфигурацию:\n<code>{config_link}</code>\n\n📊 Трафик:\nЛимит: <b>{traffic_limit}</b>\nИспользовано: <b>{traffic_used}</b>\nОсталось: <b>{traffic_left}</b>",
|
||||||
|
"traffic_no_expiry": "без ограничения",
|
||||||
"autorenew_enable_button": "🔄 Включить автопродление",
|
"autorenew_enable_button": "🔄 Включить автопродление",
|
||||||
"autorenew_disable_button": "🛑 Отключить автопродление",
|
"autorenew_disable_button": "🛑 Отключить автопродление",
|
||||||
"subscription_autorenew_updated": "Настройки автопродления обновлены.",
|
"subscription_autorenew_updated": "Настройки автопродления обновлены.",
|
||||||
@@ -439,6 +453,7 @@
|
|||||||
"payment_method_tx_history_title": "📜 История операций",
|
"payment_method_tx_history_title": "📜 История операций",
|
||||||
"payment_method_no_history": "История операций отсутствует.",
|
"payment_method_no_history": "История операций отсутствует.",
|
||||||
"subscription_purchase_title": "Покупка подписки на {months} мес.",
|
"subscription_purchase_title": "Покупка подписки на {months} мес.",
|
||||||
|
"traffic_purchase_title": "Покупка {traffic_gb} ГБ",
|
||||||
"autorenew_enable_requires_card": "Прежде чем включать автоплатёж, привяжите карту в разделе «Способы оплаты».",
|
"autorenew_enable_requires_card": "Прежде чем включать автоплатёж, привяжите карту в разделе «Способы оплаты».",
|
||||||
"subscription_not_active": "У вас нет активной подписки.",
|
"subscription_not_active": "У вас нет активной подписки.",
|
||||||
"error_service_unavailable": "Сервис недоступен. Попробуйте позже.",
|
"error_service_unavailable": "Сервис недоступен. Попробуйте позже.",
|
||||||
|
|||||||
Reference in New Issue
Block a user