added gb packets selling

This commit is contained in:
machka pasla
2025-12-11 12:47:32 +03:00
parent 061fdeb72b
commit 22171eb66f
24 changed files with 798 additions and 246 deletions
+21 -4
View File
@@ -34,7 +34,7 @@ async def get_payments_with_pagination(session: AsyncSession, page: int = 0,
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."""
_ = 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',
'platega': 'Platega',
}.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 (
f"{status_emoji} <b>{payment.amount} {payment.currency}</b>\n"
f"👤 {user_info}\n"
f"💳 {provider_text}\n"
f"📅 {payment_date}\n"
f"{period_line}\n"
f"📋 {payment.status}\n"
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")
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
# 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_status", default="Status"),
_("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_provider_payment_id", default="Provider Payment ID")
])
traffic_mode = getattr(settings, "traffic_sale_mode", False)
# Write payment data
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([
payment.payment_id,
payment.user_id,
@@ -219,7 +236,7 @@ async def export_payments_csv_handler(callback: types.CallbackQuery, i18n_data:
payment.provider or "",
payment.status,
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.provider_payment_id or ""
])
+50 -22
View File
@@ -40,6 +40,8 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
metadata = payment_info_from_webhook.get("metadata", {})
user_id_str = metadata.get("user_id")
subscription_months_str = metadata.get("subscription_months")
traffic_gb_str = metadata.get("traffic_gb")
sale_mode = metadata.get("sale_mode") or ("traffic" if settings.traffic_sale_mode else "subscription")
promo_code_id_str = metadata.get("promo_code_id")
payment_db_id_str = metadata.get("payment_db_id")
auto_renew_subscription_id_str = metadata.get(
@@ -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,
# we will create/ensure a payment record idempotently using provider payment id.
if (not user_id_str or not subscription_months_str
or (not payment_db_id_str and not auto_renew_subscription_id_str)):
if (
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(
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
try:
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_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_str
) if promo_code_id_str and promo_code_id_str.isdigit() else None
amount_data = payment_info_from_webhook.get("amount", {})
months_for_record = int(subscription_months) if sale_mode != "traffic" else 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
@@ -79,9 +86,9 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
user_id=user_id,
amount=payment_value,
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") or f"Auto-renewal for {subscription_months} months",
"description") or f"Auto-renewal for {months_for_record or subscription_months} months",
provider="yookassa",
provider_payment_id=yk_payment_id_from_hook,
)
@@ -196,14 +203,18 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
raise Exception(
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(
session,
user_id,
subscription_months,
months_for_activation,
payment_value,
payment_db_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'):
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", 0)
referral_bonus_info = await referral_service.apply_referral_bonuses_for_payment(
session,
user_id,
subscription_months,
current_payment_db_id=payment_db_id,
skip_if_active_before_payment=False,
)
referral_bonus_info = None
if sale_mode != "traffic":
referral_bonus_info = await referral_service.apply_referral_bonuses_for_payment(
session,
user_id,
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
if referral_bonus_info and referral_bonus_info.get(
"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
_ = 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
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 = _(
"yookassa_auto_renewal",
months=subscription_months,
months=int(subscription_months),
end_date=final_end_date_for_user.strftime('%Y-%m-%d'),
)
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:
config_link = activation_details.get("subscription_url") or _(
"config_link_not_available"
@@ -263,7 +290,7 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
details_message = _(
"payment_successful_with_referral_bonus_full",
months=subscription_months,
months=int(subscription_months),
base_end_date=base_subscription_end_date.strftime('%Y-%m-%d'),
bonus_days=applied_referee_bonus_days_from_referral,
final_end_date=final_end_date_for_user.strftime('%Y-%m-%d'),
@@ -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:
details_message = _(
"payment_successful_with_promo_full",
months=subscription_months,
months=int(subscription_months),
bonus_days=applied_promo_bonus_days,
end_date=final_end_date_for_user.strftime('%Y-%m-%d'),
config_link=config_link,
@@ -281,7 +308,7 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
elif final_end_date_for_user:
details_message = _(
"payment_successful_full",
months=subscription_months,
months=int(subscription_months),
end_date=final_end_date_for_user.strftime('%Y-%m-%d'),
config_link=config_link,
)
@@ -315,9 +342,10 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
user_id=user_id,
amount=payment_value,
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
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:
logging.error(f"Failed to send payment notification: {e}")
+18 -16
View File
@@ -74,24 +74,26 @@ async def referral_command_handler(event: Union[types.Message,
return
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(
settings.subscription_options.items()):
inv_bonus = settings.referral_bonus_inviter.get(months_period_key)
ref_bonus = settings.referral_bonus_referee.get(months_period_key)
if inv_bonus is not None or ref_bonus is not None:
bonus_info_parts.append(
_("referral_bonus_per_period",
months=months_period_key,
inviter_bonus_days=inv_bonus
if inv_bonus is not None else _("no_bonus_placeholder"),
referee_bonus_days=ref_bonus
if ref_bonus is not None else _("no_bonus_placeholder")))
inv_bonus = settings.referral_bonus_inviter.get(months_period_key)
ref_bonus = settings.referral_bonus_referee.get(months_period_key)
if inv_bonus is not None or ref_bonus is not None:
bonus_info_parts.append(
_("referral_bonus_per_period",
months=months_period_key,
inviter_bonus_days=inv_bonus
if inv_bonus is not None else _("no_bonus_placeholder"),
referee_bonus_days=ref_bonus
if ref_bonus is not None else _("no_bonus_placeholder")))
bonus_details_str = "\n".join(bonus_info_parts) if bonus_info_parts else _(
"referral_no_bonuses_configured")
bonus_details_str = "\n".join(bonus_info_parts) if bonus_info_parts else _(
"referral_no_bonuses_configured")
# Get referral statistics
referral_stats = await referral_service.get_referral_stats(session, inviter_user_id)
+53 -16
View File
@@ -40,11 +40,15 @@ async def display_subscription_options(event: Union[types.Message, types.Callbac
return
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 = (
get_subscription_options_keyboard(settings.subscription_options, currency_symbol_val, current_lang, i18n)
if settings.subscription_options
get_subscription_options_keyboard(options, currency_symbol_val, current_lang, i18n, traffic_mode=traffic_mode)
if options
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")
days_left = (end_date.date() - datetime.now().date()).days if end_date else 0
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")
),
)
traffic_mode = bool(getattr(settings, "traffic_sale_mode", False))
def _fmt_gb(val: Optional[float]) -> str:
if val is None:
return get_text("traffic_na")
try:
if isinstance(val, (int, float)):
val_gb = float(val) / (2**30)
return f"{val_gb:.2f} GB"
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)
kb = base_markup.inline_keyboard
@@ -214,7 +251,7 @@ async def my_subscription_command_handler(
])
# 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 = (
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)
if settings.yookassa_autopayments_active:
if not traffic_mode and settings.yookassa_autopayments_active:
prepend_rows.append([
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)
return
traffic_mode = getattr(settings, "traffic_sale_mode", False)
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"
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:
pass
@@ -39,9 +39,10 @@ async def pay_crypto_callback_handler(
try:
_, data_payload = callback.data.split(":", 1)
months_str, price_str = data_payload.split(":")
months = int(months_str)
price_amount = float(price_str)
parts = data_payload.split(":")
months = float(parts[0])
price_amount = float(parts[1])
sale_mode = parts[2] if len(parts) > 2 else "subscription"
except (ValueError, IndexError):
try:
await callback.answer(get_text("error_try_again"), show_alert=True)
@@ -50,7 +51,12 @@ async def pay_crypto_callback_handler(
return
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(
session=session,
@@ -58,17 +64,22 @@ async def pay_crypto_callback_handler(
months=months,
amount=price_amount,
description=payment_description,
sale_mode=sale_mode,
)
if invoice_url:
try:
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(
invoice_url,
current_lang,
i18n,
back_callback=f"subscribe_period:{months}",
back_callback=f"subscribe_period:{human_value}",
back_text_key="back_to_payment_methods_button",
),
disable_web_page_preview=False,
@@ -76,12 +87,16 @@ async def pay_crypto_callback_handler(
except Exception:
try:
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(
invoice_url,
current_lang,
i18n,
back_callback=f"subscribe_period:{months}",
back_callback=f"subscribe_period:{human_value}",
back_text_key="back_to_payment_methods_button",
),
disable_web_page_preview=False,
@@ -47,9 +47,10 @@ async def pay_fk_callback_handler(
try:
_, data_payload = callback.data.split(":", 1)
months_str, price_str = data_payload.split(":")
months = int(months_str)
price_rub = float(price_str)
parts = data_payload.split(":")
months = float(parts[0])
price_rub = float(parts[1])
sale_mode = parts[2] if len(parts) > 2 else "subscription"
except (ValueError, IndexError):
logging.error(f"Invalid pay_fk data in callback: {callback.data}")
try:
@@ -59,7 +60,12 @@ async def pay_fk_callback_handler(
return
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"
payment_record_payload = {
@@ -68,7 +74,7 @@ async def pay_fk_callback_handler(
"currency": currency_code,
"status": "pending_freekassa",
"description": payment_description,
"subscription_duration_months": months,
"subscription_duration_months": int(months),
"provider": "freekassa",
}
@@ -135,12 +141,16 @@ async def pay_fk_callback_handler(
)
try:
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(
location,
current_lang,
i18n,
back_callback=f"subscribe_period:{months}",
back_callback=f"subscribe_period:{human_value}",
back_text_key="back_to_payment_methods_button",
),
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.")
try:
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(
location,
current_lang,
i18n,
back_callback=f"subscribe_period:{months}",
back_callback=f"subscribe_period:{human_value}",
back_text_key="back_to_payment_methods_button",
),
disable_web_page_preview=False,
@@ -47,9 +47,10 @@ async def pay_platega_callback_handler(
try:
_, data_payload = callback.data.split(":", 1)
months_str, price_str = data_payload.split(":")
months = int(months_str)
price_rub = float(price_str)
parts = data_payload.split(":")
months = float(parts[0])
price_rub = float(parts[1])
sale_mode = parts[2] if len(parts) > 2 else "subscription"
except (ValueError, IndexError):
logging.error(f"Invalid pay_platega data in callback: {callback.data}")
try:
@@ -59,7 +60,12 @@ async def pay_platega_callback_handler(
return
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"
payment_record_payload = {
@@ -68,7 +74,7 @@ async def pay_platega_callback_handler(
"currency": currency_code,
"status": "pending_platega",
"description": payment_description,
"subscription_duration_months": months,
"subscription_duration_months": int(months),
"provider": "platega",
}
@@ -96,6 +102,7 @@ async def pay_platega_callback_handler(
"payment_db_id": payment_record.payment_id,
"user_id": user_id,
"months": months,
"sale_mode": sale_mode,
}
)
@@ -136,12 +143,16 @@ async def pay_platega_callback_handler(
try:
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(
redirect_url,
current_lang,
i18n,
back_callback=f"subscribe_period:{months}",
back_callback=f"subscribe_period:{human_value}",
back_text_key="back_to_payment_methods_button",
),
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.")
try:
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(
redirect_url,
current_lang,
i18n,
back_callback=f"subscribe_period:{months}",
back_callback=f"subscribe_period:{human_value}",
back_text_key="back_to_payment_methods_button",
),
disable_web_page_preview=False,
@@ -46,9 +46,10 @@ async def pay_severpay_callback_handler(
try:
_, data_payload = callback.data.split(":", 1)
months_str, price_str = data_payload.split(":")
months = int(months_str)
price_rub = float(price_str)
parts = data_payload.split(":")
months = float(parts[0])
price_rub = float(parts[1])
sale_mode = parts[2] if len(parts) > 2 else "subscription"
except (ValueError, IndexError):
logging.error(f"Invalid pay_severpay data in callback: {callback.data}")
try:
@@ -58,7 +59,12 @@ async def pay_severpay_callback_handler(
return
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"
payment_record_payload = {
@@ -67,7 +73,7 @@ async def pay_severpay_callback_handler(
"currency": currency_code,
"status": "pending_severpay",
"description": payment_description,
"subscription_duration_months": months,
"subscription_duration_months": int(months),
"provider": "severpay",
}
@@ -126,12 +132,16 @@ async def pay_severpay_callback_handler(
if payment_link:
try:
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(
payment_link,
current_lang,
i18n,
back_callback=f"subscribe_period:{months}",
back_callback=f"subscribe_period:{human_value}",
back_text_key="back_to_payment_methods_button",
),
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.")
try:
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(
payment_link,
current_lang,
i18n,
back_callback=f"subscribe_period:{months}",
back_callback=f"subscribe_period:{human_value}",
back_text_key="back_to_payment_methods_button",
),
disable_web_page_preview=False,
@@ -40,9 +40,10 @@ async def pay_stars_callback_handler(
try:
_, data_payload = callback.data.split(":", 1)
months_str, stars_price_str = data_payload.split(":")
months = int(months_str)
stars_price = int(stars_price_str)
parts = data_payload.split(":")
months = float(parts[0])
stars_price = int(float(parts[1]))
sale_mode = parts[2] if len(parts) > 2 else "subscription"
except (ValueError, IndexError):
try:
await callback.answer(get_text("error_try_again"), show_alert=True)
@@ -51,7 +52,12 @@ async def pay_stars_callback_handler(
return
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(
session=session,
@@ -59,16 +65,21 @@ async def pay_stars_callback_handler(
months=months,
stars_price=stars_price,
description=payment_description,
sale_mode=sale_mode,
)
if payment_db_id:
try:
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=[
[InlineKeyboardButton(
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
if message and message.successful_payment else "")
try:
payment_db_id_str, months_str = (payload or "").split(":", 1)
payment_db_id = int(payment_db_id_str)
months = int(months_str)
parts = (payload or "").split(":")
payment_db_id = int(parts[0])
months = float(parts[1]) if len(parts) > 1 else 0
sale_mode = parts[2] if len(parts) > 2 else "subscription"
except Exception:
return
@@ -120,4 +132,5 @@ async def handle_successful_stars_payment(
months=months,
stars_amount=stars_amount,
i18n_data=i18n_data,
sale_mode=sale_mode,
)
@@ -29,8 +29,9 @@ async def select_subscription_period_callback_handler(
pass
return
traffic_mode = bool(getattr(settings, "traffic_sale_mode", False))
try:
months = int(callback.data.split(":")[-1])
months = float(callback.data.split(":")[-1])
except (ValueError, IndexError):
logging.error(f"Invalid subscription period in callback_data: {callback.data}")
try:
@@ -39,10 +40,13 @@ async def select_subscription_period_callback_handler(
pass
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:
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:
await callback.answer(get_text("error_try_again"), show_alert=True)
@@ -51,8 +55,8 @@ async def select_subscription_period_callback_handler(
return
currency_symbol_val = settings.DEFAULT_CURRENCY_SYMBOL
text_content = get_text("choose_payment_method")
stars_price = settings.stars_subscription_options.get(months)
text_content = get_text("choose_payment_method_traffic") if traffic_mode else get_text("choose_payment_method")
stars_price = stars_price_source.get(months)
reply_markup = get_payment_method_keyboard(
months,
price_rub,
@@ -61,6 +65,7 @@ async def select_subscription_period_callback_handler(
current_lang,
i18n,
settings,
sale_mode="traffic" if traffic_mode else "subscription",
)
try:
@@ -18,10 +18,17 @@ from db.dal import payment_dal, user_billing_dal
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:
months_str, price_str = payload.split(":")
return int(months_str), float(price_str)
parts = payload.split(":")
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):
return None
@@ -64,19 +71,24 @@ async def _initiate_yk_payment(
back_callback: str,
payment_method_id: Optional[str] = None,
selected_method_internal_id: Optional[int] = None,
sale_mode: str = "subscription",
) -> bool:
"""Create payment record and initiate YooKassa payment (new card or saved card)."""
if not callback.message:
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 = {
"user_id": user_id,
"amount": price_rub,
"currency": currency_code_for_yk,
"status": "pending_yookassa",
"description": payment_description,
"subscription_duration_months": months,
"subscription_duration_months": int(months),
}
db_payment_record = None
@@ -109,7 +121,10 @@ async def _initiate_yk_payment(
"user_id": str(user_id),
"subscription_months": str(months),
"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:
yookassa_metadata["used_saved_payment_method_id"] = payment_method_id
@@ -198,7 +213,11 @@ async def _initiate_yk_payment(
try:
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(
payment_response_yk["confirmation_url"],
current_lang,
@@ -214,7 +233,11 @@ async def _initiate_yk_payment(
)
try:
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(
payment_response_yk["confirmation_url"],
current_lang,
@@ -328,7 +351,7 @@ async def pay_yk_callback_handler(callback: types.CallbackQuery, settings: Setti
pass
return
parsed = _parse_months_and_price(data_payload)
parsed = _parse_offer_payload(data_payload)
if not parsed:
logging.error(f"Invalid pay_yk payload structure: {callback.data}")
try:
@@ -337,10 +360,10 @@ async def pay_yk_callback_handler(callback: types.CallbackQuery, settings: Setti
pass
return
months, price_rub = parsed
months, price_rub, sale_mode = parsed
user_id = callback.from_user.id
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(
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,
i18n,
has_saved_cards=True,
sale_mode=sale_mode,
),
)
except Exception as e_edit:
@@ -377,6 +401,7 @@ async def pay_yk_callback_handler(callback: types.CallbackQuery, settings: Setti
current_lang,
i18n,
has_saved_cards=True,
sale_mode=sale_mode,
),
)
except Exception:
@@ -400,7 +425,8 @@ async def pay_yk_callback_handler(callback: types.CallbackQuery, settings: Setti
price_rub=price_rub,
currency_code_for_yk=currency_code_for_yk,
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:
await callback.answer()
@@ -443,7 +469,7 @@ async def pay_yk_new_card_handler(callback: types.CallbackQuery, settings: Setti
pass
return
parsed = _parse_months_and_price(data_payload)
parsed = _parse_offer_payload(data_payload)
if not parsed:
logging.error(f"Invalid pay_yk_new payload structure: {callback.data}")
try:
@@ -452,10 +478,10 @@ async def pay_yk_new_card_handler(callback: types.CallbackQuery, settings: Setti
pass
return
months, price_rub = parsed
months, price_rub, sale_mode = parsed
user_id = callback.from_user.id
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(
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,
currency_code_for_yk=currency_code_for_yk,
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:
await callback.answer()
@@ -494,14 +521,6 @@ async def pay_yk_saved_list_handler(callback: types.CallbackQuery, settings: Set
pass
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:
_, data_payload = callback.data.split(":", 1)
except ValueError:
@@ -522,9 +541,10 @@ async def pay_yk_saved_list_handler(callback: types.CallbackQuery, settings: Set
return
try:
months = int(parts[0])
months = float(parts[0])
price_rub = float(parts[1])
page = int(parts[2]) if len(parts) > 2 else 0
sale_mode = parts[3] if len(parts) > 3 else "subscription"
except (ValueError, IndexError):
logging.error(f"pay_yk_saved_list payload parsing error: {callback.data}")
try:
@@ -533,6 +553,14 @@ async def pay_yk_saved_list_handler(callback: types.CallbackQuery, settings: Set
pass
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
try:
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,
i18n,
has_saved_cards=False,
sale_mode=sale_mode,
),
)
except Exception as e_edit:
@@ -565,6 +594,7 @@ async def pay_yk_saved_list_handler(callback: types.CallbackQuery, settings: Set
current_lang,
i18n,
has_saved_cards=False,
sale_mode=sale_mode,
),
)
except Exception:
@@ -596,6 +626,7 @@ async def pay_yk_saved_list_handler(callback: types.CallbackQuery, settings: Set
current_lang,
i18n,
page=page,
sale_mode=sale_mode,
),
)
except Exception as e_edit:
@@ -610,6 +641,7 @@ async def pay_yk_saved_list_handler(callback: types.CallbackQuery, settings: Set
current_lang,
i18n,
page=page,
sale_mode=sale_mode,
),
)
except Exception:
@@ -633,14 +665,6 @@ async def pay_yk_use_saved_handler(callback: types.CallbackQuery, settings: Sett
pass
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:
logging.error("YooKassa service unavailable for pay_yk_use_saved.")
try:
@@ -673,8 +697,9 @@ async def pay_yk_use_saved_handler(callback: types.CallbackQuery, settings: Sett
return
try:
months = int(parts[0])
months = float(parts[0])
price_rub = float(parts[1])
sale_mode = parts[3] if len(parts) > 3 else "subscription"
except (ValueError, IndexError):
logging.error(f"pay_yk_use_saved months/price parsing error: {callback.data}")
try:
@@ -683,6 +708,14 @@ async def pay_yk_use_saved_handler(callback: types.CallbackQuery, settings: Sett
pass
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]
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,
currency_code_for_yk=currency_code_for_yk,
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,
selected_method_internal_id=selected_method.method_id,
sale_mode=sale_mode,
)
try:
await callback.answer()