Merge pull request #100 from zerodata731/freekassa-codex

Freekassa codex
This commit is contained in:
machka pasla
2025-10-14 09:38:47 +03:00
committed by GitHub
13 changed files with 145 additions and 194 deletions
+7 -8
View File
@@ -24,6 +24,13 @@ DISABLE_WELCOME_MESSAGE= #
# Webhook Base URL (used for Telegram and payment providers)
WEBHOOK_BASE_URL=https://webhooks.yourdomain.tld
# Payment Method Toggles
YOOKASSA_ENABLED=True # Turn on YOOKASSA
FREEKASSA_ENABLED=True # Turn on FreeKassa
STARS_ENABLED=True # Turn on STARS
TRIBUTE_ENABLED=True # Turn on TRIBUTE
CRYPTOPAY_ENABLED=True # Turn on CRYPTOPAY
# YooKassa Payment Gateway Configuration
YOOKASSA_SHOP_ID=your_shop_id # Your store ID in YooKassa
YOOKASSA_SECRET_KEY=your_secret_key # Your secret key for YooKassa
@@ -33,11 +40,9 @@ YOOKASSA_VAT_CODE=1 #
YOOKASSA_AUTOPAYMENTS_ENABLED=False # Auto-renew toggle
# FreeKassa Payment Gateway Configuration
FREEKASSA_ENABLED=True # Turn on FreeKassa
FREEKASSA_MERCHANT_ID=your_shop_id # Your shop ID in FreeKassa
FREEKASSA_API_KEY=your_api_key # API key for REST requests
FREEKASSA_SECOND_SECRET=your_second_secret # Secret word #2 (used to verify notifications)
FREEKASSA_CURRENCY=RUB # Default currency for orders
FREEKASSA_PAYMENT_IP= # Public IP address reported to FreeKassa
# CryptoBot Payment Gateway Configuration
@@ -51,12 +56,6 @@ TRIBUTE_API_KEY= #
TRIBUTE_SKIP_NOTIFICATIONS=True # Skip renewal notifications for Tribute payments
TRIBUTE_SKIP_CANCELLATION_NOTIFICATIONS=False # Skip cancellation notifications for Tribute payments
# Payment Method Toggles
YOOKASSA_ENABLED=True # Turn on YOOKASSA
STARS_ENABLED=True # Turn on STARS
TRIBUTE_ENABLED=True # Turn on TRIBUTE
CRYPTOPAY_ENABLED=True # Turn on CRYPTOPAY
# Subscription Options. Specify cost parameters or payment links here.
1_MONTH_ENABLED=True
RUB_PRICE_1_MONTH=150
-1
View File
@@ -86,7 +86,6 @@
| `FREEKASSA_API_KEY` | API-ключ для запросов к FreeKassa REST API. |
| `FREEKASSA_SECOND_SECRET` | Секретное слово №2 — используется для проверки уведомлений от FreeKassa. |
| `FREEKASSA_PAYMENT_URL` | (Опционально, legacy SCI) Базовый URL платёжной формы FreeKassa. По умолчанию `https://pay.freekassa.ru/`. |
| `FREEKASSA_CURRENCY` | Код валюты платежа (например, `RUB`). |
| `FREEKASSA_PAYMENT_IP` | Внешний IP вашего сервера, который будет передаваться в запрос оплаты. |
| `STARS_ENABLED` | Включить/выключить Telegram Stars (`true`/`false`). |
| `TRIBUTE_ENABLED`| Включить/выключить Tribute (`true`/`false`). |
+1 -1
View File
@@ -280,7 +280,7 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
details_message = _("payment_successful_error_details")
details_markup = get_connect_and_main_keyboard(
user_lang, i18n, settings, config_link
user_lang, i18n, settings, config_link, preserve_message=True
)
try:
await bot.send_message(
+7
View File
@@ -427,6 +427,13 @@ async def main_action_callback_handler(
subscription_service,
session,
is_edit=True)
elif action == "back_to_main_keep":
await send_main_menu(callback,
settings,
i18n_data,
subscription_service,
session,
is_edit=False)
else:
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
_ = lambda key, **kwargs: i18n.gettext(
+64 -130
View File
@@ -1,6 +1,7 @@
import logging
from datetime import datetime
from aiogram import Router, F, types
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession
@@ -239,7 +240,13 @@ async def pay_yk_callback_handler(callback: types.CallbackQuery, settings: Setti
await callback.message.edit_text(
get_text(key="payment_link_message", months=months),
reply_markup=get_payment_url_keyboard(payment_response_yk["confirmation_url"], current_lang, i18n),
reply_markup=get_payment_url_keyboard(
payment_response_yk["confirmation_url"],
current_lang,
i18n,
back_callback=f"subscribe_period:{months}",
back_text_key="back_to_payment_methods_button",
),
disable_web_page_preview=False,
)
else:
@@ -340,139 +347,24 @@ async def pay_fk_callback_handler(
pass
return
method_keyboard = InlineKeyboardBuilder()
method_keyboard.button(
text=get_text("freekassa_method_qr"),
callback_data=f"pay_fk_method:{payment_record.payment_id}:44",
)
method_keyboard.button(
text=get_text("freekassa_method_card"),
callback_data=f"pay_fk_method:{payment_record.payment_id}:36",
)
method_keyboard.button(
text=get_text("freekassa_method_sberpay"),
callback_data=f"pay_fk_method:{payment_record.payment_id}:43",
)
method_keyboard.button(
text=get_text("back_to_main_menu_button"),
callback_data="main_action:subscribe",
)
method_keyboard.adjust(1)
try:
await callback.message.edit_text(
get_text("freekassa_choose_method"),
reply_markup=method_keyboard.as_markup(),
)
except Exception as e_edit:
logging.warning(f"FreeKassa: failed to show method selector ({e_edit}), sending new message.")
try:
await callback.message.answer(
get_text("freekassa_choose_method"),
reply_markup=method_keyboard.as_markup(),
)
except Exception:
pass
try:
await callback.answer()
except Exception:
pass
@router.callback_query(F.data.startswith("pay_fk_method:"))
async def pay_fk_method_callback_handler(
callback: types.CallbackQuery,
settings: Settings,
i18n_data: dict,
freekassa_service: FreeKassaService,
session: AsyncSession,
):
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
get_text = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key
if not i18n or not callback.message:
try:
await callback.answer(get_text("error_occurred_try_again"), show_alert=True)
except Exception:
pass
return
if not freekassa_service or not freekassa_service.configured:
try:
await callback.answer(get_text("payment_service_unavailable_alert"), show_alert=True)
except Exception:
pass
try:
await callback.message.edit_text(get_text("payment_service_unavailable"))
except Exception:
pass
return
try:
_, payload = callback.data.split(":", 1)
payment_id_str, method_code = payload.split(":")
payment_id = int(payment_id_str)
except (ValueError, IndexError):
logging.error(f"FreeKassa: invalid method payload {callback.data}")
try:
await callback.answer(get_text("error_try_again"), show_alert=True)
except Exception:
pass
return
try:
payment_record = await payment_dal.get_payment_by_db_id(session, payment_id)
except Exception as e_db:
logging.error(f"FreeKassa: failed to load payment {payment_id}: {e_db}")
payment_record = None
if not payment_record:
try:
await callback.answer(get_text("error_payment_gateway"), show_alert=True)
except Exception:
pass
return
if payment_record.user_id != callback.from_user.id:
logging.warning(
f"FreeKassa: user {callback.from_user.id} attempted to access payment {payment_id} owned by {payment_record.user_id}"
)
try:
await callback.answer(get_text("error_payment_gateway"), show_alert=True)
except Exception:
pass
return
months = payment_record.subscription_duration_months or 1
amount = float(payment_record.amount)
try:
method_code_int = int(method_code)
except (TypeError, ValueError):
logging.error(f"FreeKassa: invalid method code {method_code} for payment {payment_record.payment_id}")
try:
await callback.answer(get_text("error_payment_gateway"), show_alert=True)
except Exception:
pass
return
success, response_data = await freekassa_service.create_order(
payment_db_id=payment_record.payment_id,
user_id=payment_record.user_id,
months=months,
amount=amount,
currency=settings.DEFAULT_CURRENCY_SYMBOL or "RUB",
method_code=method_code_int,
amount=price_rub,
currency=freekassa_service.default_currency,
method_code=44,
ip_address=freekassa_service.server_ip,
extra_params={
"us_method": method_code_int,
"us_method": 44,
},
)
if success:
location = response_data.get("location")
provider_identifier = response_data.get("orderHash") or response_data.get("orderId")
order_hash = response_data.get("orderHash")
order_id_api = response_data.get("orderId")
provider_identifier = order_hash or order_id_api
if provider_identifier:
try:
@@ -491,18 +383,36 @@ async def pay_fk_method_callback_handler(
)
if location:
order_identifier_display = str(order_id_api or provider_identifier or payment_record.payment_id)
order_info_text = get_text(
"free_kassa_order_info",
order_id=order_identifier_display,
date=datetime.now().strftime("%Y-%m-%d"),
)
try:
await callback.message.edit_text(
get_text(key="payment_link_message", months=months),
reply_markup=get_payment_url_keyboard(location, current_lang, i18n),
f"{order_info_text}\n\n" + get_text(key="payment_link_message", months=months),
reply_markup=get_payment_url_keyboard(
location,
current_lang,
i18n,
back_callback=f"subscribe_period:{months}",
back_text_key="back_to_payment_methods_button",
),
disable_web_page_preview=False,
)
except Exception as e_edit:
logging.warning(f"FreeKassa: failed to display payment link ({e_edit}), sending new message.")
try:
await callback.message.answer(
get_text(key="payment_link_message", months=months),
reply_markup=get_payment_url_keyboard(location, current_lang, i18n),
f"{order_info_text}\n\n" + get_text(key="payment_link_message", months=months),
reply_markup=get_payment_url_keyboard(
location,
current_lang,
i18n,
back_callback=f"subscribe_period:{months}",
back_text_key="back_to_payment_methods_button",
),
disable_web_page_preview=False,
)
except Exception:
@@ -599,14 +509,26 @@ async def pay_crypto_callback_handler(
try:
await callback.message.edit_text(
get_text(key="payment_link_message", months=months),
reply_markup=get_payment_url_keyboard(invoice_url, current_lang, i18n),
reply_markup=get_payment_url_keyboard(
invoice_url,
current_lang,
i18n,
back_callback=f"subscribe_period:{months}",
back_text_key="back_to_payment_methods_button",
),
disable_web_page_preview=False,
)
except Exception:
try:
await callback.message.answer(
get_text(key="payment_link_message", months=months),
reply_markup=get_payment_url_keyboard(invoice_url, current_lang, i18n),
reply_markup=get_payment_url_keyboard(
invoice_url,
current_lang,
i18n,
back_callback=f"subscribe_period:{months}",
back_text_key="back_to_payment_methods_button",
),
disable_web_page_preview=False,
)
except Exception:
@@ -673,6 +595,18 @@ async def pay_stars_callback_handler(
)
if payment_db_id:
try:
await callback.message.edit_text(
get_text("payment_invoice_sent_message", months=months),
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(
text=get_text("back_to_payment_methods_button"),
callback_data=f"subscribe_period:{months}",
)]
]),
)
except Exception as e_edit:
logging.warning(f"Stars payment: failed to show invoice info message ({e_edit})")
try:
await callback.answer()
except Exception:
+23 -14
View File
@@ -118,17 +118,17 @@ def get_payment_method_keyboard(months: int, price: float,
i18n_instance, settings: Settings) -> InlineKeyboardMarkup:
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
builder = InlineKeyboardBuilder()
if settings.STARS_ENABLED and stars_price is not None:
builder.button(text=_("pay_with_stars_button"),
callback_data=f"pay_stars:{months}:{stars_price}")
if settings.TRIBUTE_ENABLED and tribute_url:
builder.button(text=_("pay_with_tribute_button"), url=tribute_url)
if settings.FREEKASSA_ENABLED:
builder.button(text=_("pay_with_sbp_button"),
callback_data=f"pay_fk:{months}:{price}")
if settings.YOOKASSA_ENABLED:
builder.button(text=_("pay_with_yookassa_button"),
callback_data=f"pay_yk:{months}:{price}")
if settings.FREEKASSA_ENABLED:
builder.button(text=_("pay_with_freekassa_button"),
callback_data=f"pay_fk:{months}:{price}")
if settings.TRIBUTE_ENABLED and tribute_url:
builder.button(text=_("pay_with_tribute_button"), url=tribute_url)
if settings.STARS_ENABLED and stars_price is not None:
builder.button(text=_("pay_with_stars_button"),
callback_data=f"pay_stars:{months}:{stars_price}")
if settings.CRYPTOPAY_ENABLED:
builder.button(text=_("pay_with_cryptopay_button"),
callback_data=f"pay_crypto:{months}:{price}")
@@ -138,13 +138,20 @@ def get_payment_method_keyboard(months: int, price: float,
return builder.as_markup()
def get_payment_url_keyboard(payment_url: str, lang: str,
i18n_instance) -> InlineKeyboardMarkup:
def get_payment_url_keyboard(payment_url: str,
lang: str,
i18n_instance,
back_callback: Optional[str] = None,
back_text_key: str = "back_to_main_menu_button"
) -> InlineKeyboardMarkup:
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
builder = InlineKeyboardBuilder()
builder.button(text=_(key="pay_button"), url=payment_url)
builder.button(text=_(key="back_to_main_menu_button"),
callback_data="main_action:back_to_main")
if back_callback:
builder.button(text=_(key=back_text_key), callback_data=back_callback)
else:
builder.button(text=_(key="back_to_main_menu_button"),
callback_data="main_action:back_to_main")
builder.adjust(1)
return builder.as_markup()
@@ -192,7 +199,8 @@ def get_connect_and_main_keyboard(
lang: str,
i18n_instance,
settings: Settings,
config_link: Optional[str]) -> InlineKeyboardMarkup:
config_link: Optional[str],
preserve_message: bool = False) -> InlineKeyboardMarkup:
"""Keyboard with a connect button and a back to main menu button."""
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
builder = InlineKeyboardBuilder()
@@ -216,10 +224,11 @@ def get_connect_and_main_keyboard(
)
)
back_callback = "main_action:back_to_main_keep" if preserve_message else "main_action:back_to_main"
builder.row(
InlineKeyboardButton(
text=_("back_to_main_menu_button"),
callback_data="main_action:back_to_main",
callback_data=back_callback,
)
)
+3 -1
View File
@@ -209,7 +209,9 @@ class CryptoPayService:
end_date=final_end.strftime('%Y-%m-%d'),
config_link=config_link)
markup = get_connect_and_main_keyboard(lang, i18n, settings, config_link)
markup = get_connect_and_main_keyboard(
lang, i18n, settings, config_link, preserve_message=True
)
try:
await bot.send_message(
user_id,
+16 -4
View File
@@ -1,4 +1,5 @@
import asyncio
from datetime import datetime
import hashlib
import hmac
import json
@@ -42,9 +43,7 @@ class FreeKassaService:
self.shop_id: Optional[str] = settings.FREEKASSA_MERCHANT_ID
self.api_key: Optional[str] = settings.FREEKASSA_API_KEY
self.second_secret: Optional[str] = settings.FREEKASSA_SECOND_SECRET
self.default_currency: str = (
settings.FREEKASSA_CURRENCY or settings.DEFAULT_CURRENCY_SYMBOL or "RUB"
).upper()
self.default_currency: str = (settings.DEFAULT_CURRENCY_SYMBOL or "RUB").upper()
self.server_ip: Optional[str] = settings.FREEKASSA_PAYMENT_IP
self.api_base_url: str = "https://api.fk.life/v1"
@@ -360,8 +359,21 @@ class FreeKassaService:
end_date=end_date_str,
config_link=config_link,
)
if provider_payment_id:
order_info_text = _(
"free_kassa_order_full",
order_id=provider_payment_id,
date=datetime.now().strftime("%Y-%m-%d"),
)
text = f"{order_info_text}\n{text}"
markup = get_connect_and_main_keyboard(lang, self.i18n, self.settings, config_link)
markup = get_connect_and_main_keyboard(
lang,
self.i18n,
self.settings,
config_link,
preserve_message=True,
)
try:
await self.bot.send_message(
payment.user_id,
+1 -1
View File
@@ -148,7 +148,7 @@ class StarsService:
config_link=config_link,
)
markup = get_connect_and_main_keyboard(
current_lang, i18n, self.settings, config_link
current_lang, i18n, self.settings, config_link, preserve_message=True
)
try:
await self.bot.send_message(
+5 -1
View File
@@ -208,7 +208,11 @@ class TributeService:
config_link=config_link,
)
markup = get_connect_and_main_keyboard(
lang, i18n, settings, config_link
lang,
i18n,
settings,
config_link,
preserve_message=True,
)
try:
+4 -1
View File
@@ -49,7 +49,6 @@ class Settings(BaseSettings):
FREEKASSA_FIRST_SECRET: Optional[str] = None
FREEKASSA_SECOND_SECRET: Optional[str] = None
FREEKASSA_PAYMENT_URL: str = Field(default="https://pay.freekassa.ru/")
FREEKASSA_CURRENCY: str = Field(default="RUB")
FREEKASSA_API_KEY: Optional[str] = None
FREEKASSA_PAYMENT_IP: Optional[str] = None
@@ -400,6 +399,10 @@ def get_settings() -> Settings:
logging.warning(
"WARNING: FreeKassa second secret is not set. Incoming payment notifications cannot be verified."
)
if not _settings_instance.subscription_options:
logging.warning(
"CRITICAL: FreeKassa is enabled but no subscription prices are configured (RUB_PRICE_*). Users will not see payment buttons."
)
except ValidationError as e:
logging.critical(
+6 -15
View File
@@ -1,7 +1,6 @@
{
"welcome": "Welcome, {user_name}!",
"main_menu_greeting": "Hi, {user_name}! 👋\nWhat would you like to do?",
"menu_activate_trial_button": "🆓 Free Trial",
"menu_subscribe_inline": "🚀 Purchase",
"menu_my_subscription_inline": "🔐 My Subscription",
@@ -13,42 +12,35 @@
"menu_server_status_button": "📊 Status",
"menu_support_button": "💬 Support",
"menu_terms_button": "📄 Terms of Service",
"back_to_main_menu_button": "⬅️ Back",
"choose_language": "Choose language:",
"language_set_alert": "Language changed!",
"error_occurred_try_again": "An error occurred, please try again.",
"error_try_again": "Please try again.",
"error_displaying_menu": "Error displaying menu.",
"main_menu_unknown_action": "Unknown action.",
"select_subscription_period": "Select subscription period:",
"subscribe_for_months_button": "{months} mo. - {price} {currency_symbol}",
"choose_payment_method": "Choose payment method:",
"pay_button": "💳 Pay",
"pay_with_yookassa_button": "💳 YooKassa",
"pay_with_freekassa_button": "💳 FreeKassa",
"pay_with_sbp_button": "📱 SBP",
"back_to_payment_methods_button": "⬅️ Back to payment methods",
"pay_with_cryptopay_button": "💎 CryptoBot",
"pay_with_tribute_button": "❤️ Tribute",
"pay_with_stars_button": "🌟 Telegram Stars",
"freekassa_choose_method": "Choose how to pay with FreeKassa:",
"freekassa_method_qr": "📱 SBP QR (i=44)",
"freekassa_method_card": "💳 Bank Card (i=36)",
"freekassa_method_sberpay": "🏦 SberPay (i=43)",
"connect_button": "🔗 Connect",
"cancel_button": "❌ Cancel",
"payment_description_subscription": "Subscription payment for {months} mo.",
"payment_link_message": "To pay for {months} mo. subscription, click the button below:",
"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_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_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.",
"config_link_not_available": "not available, contact support",
"traffic_unlimited": "Unlimited",
"promo_code_prompt": "Please enter your promo code:",
"promo_code_not_found": "Promo code <code>{code}</code> not found, expired, or already used the maximum number of times.",
"promo_code_already_used_by_user": "You have already used promo code <code>{code}</code>.",
@@ -70,7 +62,6 @@
"referral_bonus_inviter_notification_extended": "🎉 Congrats! Your friend {referee_name} paid for a subscription. You received {days} bonus days! Your subscription is now active until {new_end_date}.",
"referral_bonus_inviter_notification_new_sub": "🎉 Congrats! Your friend {referee_name} paid for a subscription. You received a {days}-day bonus subscription! It is active until {new_end_date}.",
"user_is_banned": "🚫 Your account is banned. Please contact support.",
"admin_panel_title": "Admin Panel",
"admin_stats_button": "📊 Statistics",
"admin_broadcast_button": "📢 Broadcast",
@@ -327,7 +318,6 @@
"admin_user_subscription_active_until": "⏰ <b>Active until:</b>",
"admin_user_subscription_error": "Loading error",
"admin_promo_management_button": "🎟 Promo Management",
"admin_promo_management_title": "🎟 <b>Promo Code Management</b>\n\nSelect a promo code for detailed view:",
"admin_promo_management_empty": "📭 No promo codes available",
"admin_promo_card_title": "🎟 <b>Promo Code: {code}</b>",
@@ -440,5 +430,6 @@
"admin_ads_delete_button": "🗑 Delete campaign",
"admin_ads_delete_confirm": "Are you sure you want to delete campaign #{id}? This action is irreversible.",
"admin_ads_deleted_success": "Campaign deleted.",
"admin_ads_not_found": "Campaign not found."
"admin_ads_not_found": "Campaign not found.",
"free_kassa_order_full": "Order #{order_id} from {date}\n\n"
}
+6 -15
View File
@@ -1,7 +1,6 @@
{
"welcome": "Добро пожаловать, {user_name}!",
"main_menu_greeting": "Привет, {user_name}! 👋\nЧто бы вы хотели сделать?",
"menu_activate_trial_button": "🆓 Пробный период",
"menu_subscribe_inline": "🚀 Купить",
"menu_my_subscription_inline": "🔐 Моя подписка",
@@ -13,42 +12,35 @@
"menu_server_status_button": "📊 Статус",
"menu_support_button": "💬 Поддержка",
"menu_terms_button": "📄 Условия сервиса",
"back_to_main_menu_button": "⬅️ Назад",
"choose_language": "Выберите язык / Select language:",
"language_set_alert": "Язык изменен!",
"error_occurred_try_again": "Произошла ошибка, попробуйте снова.",
"error_try_again": "Попробуйте еще раз.",
"error_displaying_menu": "Ошибка отображения меню.",
"main_menu_unknown_action": "Неизвестное действие.",
"select_subscription_period": "Выберите срок подписки:",
"subscribe_for_months_button": "{months} мес. - {price} {currency_symbol}",
"choose_payment_method": "Выберите способ оплаты:",
"pay_button": "💳 Оплатить",
"pay_with_yookassa_button": "💳 ЮKassa",
"pay_with_freekassa_button": "💳 FreeKassa",
"pay_with_sbp_button": "📱 СБП",
"back_to_payment_methods_button": "⬅️ Назад к выбору оплаты",
"pay_with_cryptopay_button": "💎 CryptoBot",
"pay_with_tribute_button": "❤️ Tribute",
"pay_with_stars_button": "🌟 Звезды Telegram",
"freekassa_choose_method": "Выберите способ оплаты FreeKassa:",
"freekassa_method_qr": "📱 QR по СБП (i=44)",
"freekassa_method_card": "💳 Банковская карта РФ (i=36)",
"freekassa_method_sberpay": "🏦 SberPay (i=43)",
"connect_button": "🔗 Подключиться",
"cancel_button": "❌ Отмена",
"payment_description_subscription": "Оплата подписки на {months} мес.",
"payment_link_message": "Для оплаты подписки на {months} мес., нажмите кнопку ниже:",
"free_kassa_order_info": "Заказ №{order_id} от {date}",
"payment_invoice_sent_message": "Счёт Telegram Stars отправлен выше. Нажмите «Оплатить» или вернитесь к выбору способа ниже.",
"payment_successful_error_details": "✅ Оплата прошла успешно, но возникла ошибка при отображении деталей. Ваша подписка активна. Свяжитесь с поддержкой, если что-то не так.",
"payment_successful_full": "✅ Оплата прошла успешно!\nВаша подписка на {months} мес. активна до {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": "❌ Оплата не удалась или была отменена. Пожалуйста, попробуйте еще раз или свяжитесь с поддержкой.",
"config_link_not_available": "недоступна, обратитесь в поддержку",
"traffic_unlimited": "Безлимитный",
"promo_code_prompt": "Пожалуйста, введите ваш промокод:",
"promo_code_not_found": "Промокод <code>{code}</code> не найден, истек или уже использован максимальное количество раз.",
"promo_code_already_used_by_user": "Вы уже активировали промокод <code>{code}</code>.",
@@ -70,7 +62,6 @@
"referral_bonus_inviter_notification_extended": "🎉 Поздравляем! Ваш друг {referee_name} оплатил подписку. Вам начислено {days} бонусных дней! Ваша подписка теперь активна до {new_end_date}.",
"referral_bonus_inviter_notification_new_sub": "🎉 Поздравляем! Ваш друг {referee_name} оплатил подписку. Вам начислена бонусная подписка на {days} дней! Она активна до {new_end_date}.",
"user_is_banned": "🚫 Ваш аккаунт заблокирован. Пожалуйста, свяжитесь со службой поддержки.",
"admin_panel_title": "Панель администратора",
"admin_stats_button": "📊 Статистика",
"admin_broadcast_button": "📢 Рассылка",
@@ -327,7 +318,6 @@
"admin_user_subscription_active_until": "⏰ <b>Действует до:</b>",
"admin_user_subscription_error": "Ошибка загрузки",
"admin_promo_management_button": "🎟 Управление промокодами",
"admin_promo_management_title": "🎟 <b>Управление промокодами</b>\n\nВыберите промокод для детального просмотра:",
"admin_promo_management_empty": "📭 Промокоды отсутствуют",
"admin_promo_card_title": "🎟 <b>Промокод: {code}</b>",
@@ -440,5 +430,6 @@
"admin_ads_delete_button": "🗑 Удалить кампанию",
"admin_ads_delete_confirm": "Вы уверены, что хотите удалить кампанию #{id}? Это действие необратимо.",
"admin_ads_deleted_success": "Кампания удалена.",
"admin_ads_not_found": "Кампания не найдена."
"admin_ads_not_found": "Кампания не найдена.",
"free_kassa_order_full": "Заказ №{order_id} от {date}\n\n"
}