This commit is contained in:
raufakchurin
2025-10-12 15:27:48 +05:00
parent bdade47758
commit 52893a0629
8 changed files with 359 additions and 79 deletions
+8
View File
@@ -32,6 +32,14 @@ YOOKASSA_DEFAULT_RECEIPT_EMAIL=your_email@example.com #
YOOKASSA_VAT_CODE=1 # VAT code YOOKASSA_VAT_CODE=1 # VAT code
YOOKASSA_AUTOPAYMENTS_ENABLED=False # Auto-renew toggle 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 # CryptoBot Payment Gateway Configuration
CRYPTOPAY_TOKEN= # API token for CryptoPay CRYPTOPAY_TOKEN= # API token for CryptoPay
CRYPTOPAY_NETWORK=mainnet # Network (mainnet or testnet) CRYPTOPAY_NETWORK=mainnet # Network (mainnet or testnet)
+5 -4
View File
@@ -10,7 +10,7 @@
- **Пробная подписка:** Система пробных подписок для новых пользователей (активируется вручную по кнопке). - **Пробная подписка:** Система пробных подписок для новых пользователей (активируется вручную по кнопке).
- **Промокоды:** Возможность применять промокоды для получения скидок или бонусных дней. - **Промокоды:** Возможность применять промокоды для получения скидок или бонусных дней.
- **Реферальная программа:** Пользователи могут приглашать друзей и получать за это бонусные дни подписки. - **Реферальная программа:** Пользователи могут приглашать друзей и получать за это бонусные дни подписки.
- **Оплата:** Поддержка оплаты через YooKassa, FreeKassa, CryptoPay, Telegram Stars и Tribute. - **Оплата:** Поддержка оплаты через YooKassa, FreeKassa (REST API), CryptoPay, Telegram Stars и Tribute.
### Для администраторов: ### Для администраторов:
- **Защищенная админ-панель:** Доступ только для администраторов, указанных в `ADMIN_IDS`. - **Защищенная админ-панель:** Доступ только для администраторов, указанных в `ADMIN_IDS`.
@@ -83,10 +83,11 @@
| `CRYPTOPAY_TOKEN` | Токен из вашего CryptoPay App. | | `CRYPTOPAY_TOKEN` | Токен из вашего CryptoPay App. |
| `FREEKASSA_ENABLED` | Включить/выключить FreeKassa (`true`/`false`). | | `FREEKASSA_ENABLED` | Включить/выключить FreeKassa (`true`/`false`). |
| `FREEKASSA_MERCHANT_ID` | ID вашего магазина в FreeKassa. | | `FREEKASSA_MERCHANT_ID` | ID вашего магазина в FreeKassa. |
| `FREEKASSA_FIRST_SECRET` | Секретное слово №1 для формирования ссылок оплаты. | | `FREEKASSA_API_KEY` | API-ключ для запросов к FreeKassa REST API. |
| `FREEKASSA_SECOND_SECRET` | Секретное слово №2 для проверки уведомлений. | | `FREEKASSA_SECOND_SECRET` | Секретное слово №2 — используется для проверки уведомлений от FreeKassa. |
| `FREEKASSA_PAYMENT_URL` | (Опционально) Базовый URL платёжной формы FreeKassa. По умолчанию `https://pay.freekassa.ru/`. | | `FREEKASSA_PAYMENT_URL` | (Опционально, legacy SCI) Базовый URL платёжной формы FreeKassa. По умолчанию `https://pay.freekassa.ru/`. |
| `FREEKASSA_CURRENCY` | Код валюты платежа (например, `RUB`). | | `FREEKASSA_CURRENCY` | Код валюты платежа (например, `RUB`). |
| `FREEKASSA_PAYMENT_IP` | Внешний IP вашего сервера, который будет передаваться в запрос оплаты. |
| `STARS_ENABLED` | Включить/выключить Telegram Stars (`true`/`false`). | | `STARS_ENABLED` | Включить/выключить Telegram Stars (`true`/`false`). |
| `TRIBUTE_ENABLED`| Включить/выключить Tribute (`true`/`false`). | | `TRIBUTE_ENABLED`| Включить/выключить Tribute (`true`/`false`). |
</details> </details>
+179 -27
View File
@@ -1,5 +1,6 @@
import logging import logging
from aiogram import Router, F, types from aiogram import Router, F, types
from aiogram.utils.keyboard import InlineKeyboardBuilder
from typing import Optional from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -308,7 +309,7 @@ async def pay_fk_callback_handler(
user_id = callback.from_user.id user_id = callback.from_user.id
payment_description = get_text("payment_description_subscription", months=months) payment_description = get_text("payment_description_subscription", months=months)
currency_code = freekassa_service.currency 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 = {
"user_id": user_id, "user_id": user_id,
@@ -339,40 +340,191 @@ async def pay_fk_callback_handler(
pass pass
return return
payment_link = None method_keyboard = InlineKeyboardBuilder()
try: method_keyboard.button(
payment_link = freekassa_service.build_payment_link( text=get_text("freekassa_method_qr"),
payment_db_id=payment_record.payment_id, callback_data=f"pay_fk_method:{payment_record.payment_id}:44",
user_id=user_id, )
months=months, method_keyboard.button(
amount=price_rub, text=get_text("freekassa_method_card"),
) callback_data=f"pay_fk_method:{payment_record.payment_id}:36",
except Exception as e_link: )
logging.error(f"FreeKassa: failed to build payment link for payment {payment_record.payment_id}: {e_link}", exc_info=True) 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)
if payment_link: 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: try:
await callback.message.edit_text( await callback.message.answer(
get_text(key="payment_link_message", months=months), get_text("freekassa_choose_method"),
reply_markup=get_payment_url_keyboard(payment_link, current_lang, i18n), reply_markup=method_keyboard.as_markup(),
disable_web_page_preview=False,
) )
except Exception as e_edit: except Exception:
logging.warning(f"FreeKassa: edit message failed ({e_edit}), sending new message.") pass
try: try:
await callback.message.answer( await callback.answer()
get_text(key="payment_link_message", months=months), except Exception:
reply_markup=get_payment_url_keyboard(payment_link, current_lang, i18n), pass
disable_web_page_preview=False,
)
except Exception: @router.callback_query(F.data.startswith("pay_fk_method:"))
pass 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: try:
await callback.answer() await callback.answer(get_text("error_occurred_try_again"), show_alert=True)
except Exception: except Exception:
pass pass
return 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,
ip_address=freekassa_service.server_ip,
extra_params={
"us_method": method_code_int,
},
)
if success:
location = response_data.get("location")
provider_identifier = response_data.get("orderHash") or response_data.get("orderId")
if provider_identifier:
try:
await payment_dal.update_provider_payment_and_status(
session,
payment_record.payment_id,
str(provider_identifier),
payment_record.status,
)
await session.commit()
except Exception as e_status:
await session.rollback()
logging.error(
f"FreeKassa: failed to store provider order id for payment {payment_record.payment_id}: {e_status}",
exc_info=True,
)
if location:
try:
await callback.message.edit_text(
get_text(key="payment_link_message", months=months),
reply_markup=get_payment_url_keyboard(location, current_lang, i18n),
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),
disable_web_page_preview=False,
)
except Exception:
pass
try:
await callback.answer()
except Exception:
pass
return
logging.error(
"FreeKassa: create_order succeeded but no payment link returned for payment %s. Response: %s",
payment_record.payment_id,
response_data,
)
else:
logging.error(
"FreeKassa: create_order failed for payment %s with response %s",
payment_record.payment_id,
response_data,
)
try: try:
await payment_dal.update_payment_status_by_db_id( await payment_dal.update_payment_status_by_db_id(
session, session,
+145 -40
View File
@@ -1,10 +1,13 @@
import asyncio
import hashlib import hashlib
import hmac
import json
import logging import logging
import time
from decimal import Decimal, ROUND_HALF_UP from decimal import Decimal, ROUND_HALF_UP
from typing import Optional, Dict, Any from typing import Optional, Dict, Any, Tuple
from urllib.parse import urlencode
from aiohttp import web from aiohttp import ClientSession, ClientTimeout, web
from aiogram import Bot from aiogram import Bot
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
@@ -36,68 +39,162 @@ class FreeKassaService:
self.subscription_service = subscription_service self.subscription_service = subscription_service
self.referral_service = referral_service self.referral_service = referral_service
self.merchant_id: Optional[str] = settings.FREEKASSA_MERCHANT_ID self.shop_id: Optional[str] = settings.FREEKASSA_MERCHANT_ID
self.first_secret: Optional[str] = settings.FREEKASSA_FIRST_SECRET self.api_key: Optional[str] = settings.FREEKASSA_API_KEY
self.second_secret: Optional[str] = settings.FREEKASSA_SECOND_SECRET self.second_secret: Optional[str] = settings.FREEKASSA_SECOND_SECRET
self.payment_url: str = settings.FREEKASSA_PAYMENT_URL.rstrip("/") self.default_currency: str = (
self.currency: str = settings.FREEKASSA_CURRENCY.upper() if settings.FREEKASSA_CURRENCY else "RUB" settings.FREEKASSA_CURRENCY or settings.DEFAULT_CURRENCY_SYMBOL or "RUB"
).upper()
self.server_ip: Optional[str] = settings.FREEKASSA_PAYMENT_IP
self.configured: bool = bool( self.api_base_url: str = "https://api.fk.life/v1"
settings.FREEKASSA_ENABLED self._timeout = ClientTimeout(total=15)
and self.merchant_id self._session: Optional[ClientSession] = None
and self.first_secret self._nonce_lock = asyncio.Lock()
and self.second_secret self._last_nonce = int(time.time() * 1000)
)
self.configured: bool = bool(settings.FREEKASSA_ENABLED and self.shop_id and self.api_key)
if not self.configured: if not self.configured:
logging.warning("FreeKassaService initialized but not fully configured. Payments disabled.") logging.warning("FreeKassaService initialized but not fully configured. Payments disabled.")
if settings.FREEKASSA_ENABLED and not self.server_ip:
logging.warning("FreeKassaService: FREEKASSA_PAYMENT_IP is not set. Requests may be rejected by the provider.")
@staticmethod @staticmethod
def _format_amount(amount: float) -> str: def _format_amount(amount: float) -> str:
"""Format amount for signatures with two decimal places.""" """Format amount for payloads and signature with two decimal places."""
quantized = Decimal(str(amount)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) quantized = Decimal(str(amount)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
return f"{quantized:.2f}" return f"{quantized:.2f}"
def build_payment_link( async def create_order(
self, self,
*, *,
payment_db_id: int, payment_db_id: int,
user_id: int, user_id: int,
months: int, months: int,
amount: float, amount: float,
currency: Optional[str],
method_code: int,
email: Optional[str] = None,
ip_address: Optional[str] = None,
extra_params: Optional[Dict[str, Any]] = None, extra_params: Optional[Dict[str, Any]] = None,
) -> Optional[str]: ) -> Tuple[bool, Dict[str, Any]]:
if not self.configured: if not self.configured:
logging.error("FreeKassaService is not configured. Cannot build payment link.") logging.error("FreeKassaService is not configured. Cannot create order.")
return None return False, {"message": "service_not_configured"}
ip_address = ip_address or self.server_ip
if not ip_address:
logging.error("FreeKassaService: payment IP is required but not configured.")
return False, {"message": "missing_ip"}
email = email or f"{user_id}@telegram.org"
amount_str = self._format_amount(amount) amount_str = self._format_amount(amount)
signature_source = f"{self.merchant_id}:{amount_str}:{self.first_secret}:{payment_db_id}" currency_code = (currency or self.default_currency or "RUB").upper()
signature = hashlib.md5(signature_source.encode("utf-8")).hexdigest()
params: Dict[str, Any] = { payload: Dict[str, Any] = {
"m": self.merchant_id, "shopId": int(self.shop_id),
"oa": amount_str, "nonce": await self._generate_nonce(),
"o": str(payment_db_id), "paymentId": str(payment_db_id),
"currency": self.currency, "i": int(method_code),
"s": signature, "amount": amount_str,
"currency": currency_code,
"email": email,
"ip": ip_address,
"us_user_id": str(user_id), "us_user_id": str(user_id),
"us_months": str(months), "us_months": str(months),
"us_payment_db_id": str(payment_db_id),
} }
if extra_params: if extra_params:
for key, value in extra_params.items(): for key, value in extra_params.items():
if value is None: if value is None:
continue continue
params[f"us_{key}"] = value payload[key] = value
query_string = urlencode(params, doseq=False, safe=":") payload["signature"] = self._sign_payload(payload)
return f"{self.payment_url}?{query_string}"
def _validate_signature(self, merchant_order_id: str, amount: str, provided_signature: str) -> bool: session = await self._get_session()
if not self.configured: url = f"{self.api_base_url}/orders/create"
try:
async with session.post(url, json=payload) as response:
response_text = await response.text()
try:
response_data = json.loads(response_text) if response_text else {}
except json.JSONDecodeError:
logging.error("FreeKassa create_order: failed to decode JSON: %s", response_text)
return False, {"status": response.status, "message": "invalid_json", "raw": response_text}
if response.status != 200 or response_data.get("type") != "success":
logging.error(
"FreeKassa create_order: API returned error (status=%s, body=%s)",
response.status,
response_data,
)
return False, {"status": response.status, "message": response_data}
return True, response_data
except Exception as exc:
logging.error("FreeKassa create_order: request failed: %s", exc, exc_info=True)
return False, {"message": str(exc)}
async def _get_session(self) -> ClientSession:
if self._session is None or self._session.closed:
self._session = ClientSession(timeout=self._timeout)
return self._session
async def _generate_nonce(self) -> int:
async with self._nonce_lock:
candidate = int(time.time() * 1000)
if candidate <= self._last_nonce:
candidate = self._last_nonce + 1
self._last_nonce = candidate
return candidate
def _sign_payload(self, payload: Dict[str, Any]) -> str:
if not self.api_key:
raise RuntimeError("FreeKassa API key is not configured.")
items = [
(key, value)
for key, value in payload.items()
if key != "signature" and value is not None
]
items.sort(key=lambda pair: pair[0])
message = "|".join(str(value) for _, value in items)
return hmac.new(self.api_key.encode("utf-8"), message.encode("utf-8"), hashlib.sha256).hexdigest()
async def close(self) -> None:
if self._session and not self._session.closed:
await self._session.close()
def _validate_signature(
self,
merchant_order_id: str,
amount: str,
provided_signature: str,
payload: Optional[Dict[str, Any]] = None,
) -> bool:
if not provided_signature:
return False return False
signature_source = f"{self.merchant_id}:{amount}:{self.second_secret}:{merchant_order_id}"
expected_signature = hashlib.md5(signature_source.encode("utf-8")).hexdigest() if self.shop_id and self.second_secret:
return expected_signature.lower() == provided_signature.lower() signature_source = f"{self.shop_id}:{amount}:{self.second_secret}:{merchant_order_id}"
expected_signature = hashlib.md5(signature_source.encode("utf-8")).hexdigest()
if expected_signature.lower() == provided_signature.lower():
return True
if self.api_key and payload:
items = [
(key, value)
for key, value in payload.items()
if key not in {"signature", "SIGN"} and value is not None
]
items.sort(key=lambda pair: pair[0])
message = "|".join(str(value) for _, value in items)
alt_signature = hmac.new(self.api_key.encode("utf-8"), message.encode("utf-8"), hashlib.sha256).hexdigest()
if alt_signature.lower() == provided_signature.lower():
return True
return False
async def webhook_route(self, request: web.Request) -> web.Response: async def webhook_route(self, request: web.Request) -> web.Response:
if not self.configured: if not self.configured:
@@ -109,21 +206,29 @@ class FreeKassaService:
logging.error(f"FreeKassa webhook: failed to read POST data: {e}") logging.error(f"FreeKassa webhook: failed to read POST data: {e}")
return web.Response(status=400, text="bad_request") return web.Response(status=400, text="bad_request")
if not data: payload_dict: Dict[str, Any]
if data:
payload_dict = {str(k): v for k, v in data.items()}
else:
try: try:
data = await request.json() json_payload = await request.json()
payload_dict = {str(k): v for k, v in json_payload.items()} if isinstance(json_payload, dict) else {}
data = json_payload
except Exception: except Exception:
payload_dict = {}
data = {} data = {}
def _get(key: str, default: Optional[str] = None) -> Optional[str]: def _get(key: str, default: Optional[str] = None) -> Optional[str]:
return data.get(key) or data.get(key.lower()) or default if isinstance(data, dict):
return data.get(key) or data.get(key.lower()) or default
return payload_dict.get(key) or payload_dict.get(key.lower()) or default
merchant_id = _get("MERCHANT_ID") merchant_id = _get("MERCHANT_ID")
if merchant_id != self.merchant_id: if merchant_id != self.shop_id:
logging.error(f"FreeKassa webhook: merchant mismatch (got {merchant_id})") logging.error(f"FreeKassa webhook: merchant mismatch (got {merchant_id})")
return web.Response(status=403, text="merchant_mismatch") return web.Response(status=403, text="merchant_mismatch")
signature = _get("SIGN") signature = _get("SIGN") or _get("signature")
if not signature: if not signature:
logging.error("FreeKassa webhook: missing signature") logging.error("FreeKassa webhook: missing signature")
return web.Response(status=400, text="missing_signature") return web.Response(status=400, text="missing_signature")
@@ -136,7 +241,7 @@ class FreeKassaService:
logging.error("FreeKassa webhook: missing order_id or amount") logging.error("FreeKassa webhook: missing order_id or amount")
return web.Response(status=400, text="missing_data") return web.Response(status=400, text="missing_data")
if not self._validate_signature(order_id_str, amount_str, signature): if not self._validate_signature(order_id_str, amount_str, signature, payload_dict):
logging.error("FreeKassa webhook: invalid signature") logging.error("FreeKassa webhook: invalid signature")
return web.Response(status=403, text="invalid_signature") return web.Response(status=403, text="invalid_signature")
+14 -8
View File
@@ -50,6 +50,8 @@ class Settings(BaseSettings):
FREEKASSA_SECOND_SECRET: Optional[str] = None FREEKASSA_SECOND_SECRET: Optional[str] = None
FREEKASSA_PAYMENT_URL: str = Field(default="https://pay.freekassa.ru/") FREEKASSA_PAYMENT_URL: str = Field(default="https://pay.freekassa.ru/")
FREEKASSA_CURRENCY: str = Field(default="RUB") FREEKASSA_CURRENCY: str = Field(default="RUB")
FREEKASSA_API_KEY: Optional[str] = None
FREEKASSA_PAYMENT_IP: Optional[str] = None
YOOKASSA_ENABLED: bool = Field(default=True) YOOKASSA_ENABLED: bool = Field(default=True)
STARS_ENABLED: bool = Field(default=True) STARS_ENABLED: bool = Field(default=True)
@@ -386,14 +388,18 @@ def get_settings() -> Settings:
logging.warning( logging.warning(
"CRITICAL: YooKassa credentials (SHOP_ID or SECRET_KEY) are not set. Payments will not work." "CRITICAL: YooKassa credentials (SHOP_ID or SECRET_KEY) are not set. Payments will not work."
) )
if _settings_instance.FREEKASSA_ENABLED and ( if _settings_instance.FREEKASSA_ENABLED:
not _settings_instance.FREEKASSA_MERCHANT_ID if (
or not _settings_instance.FREEKASSA_FIRST_SECRET not _settings_instance.FREEKASSA_MERCHANT_ID
or not _settings_instance.FREEKASSA_SECOND_SECRET or not _settings_instance.FREEKASSA_API_KEY
): ):
logging.warning( logging.warning(
"CRITICAL: FreeKassa is enabled but credentials are incomplete (merchant ID or secret words). FreeKassa payments will not work." "CRITICAL: FreeKassa is enabled but SHOP_ID or API key is missing. FreeKassa payments will not work."
) )
if not _settings_instance.FREEKASSA_SECOND_SECRET:
logging.warning(
"WARNING: FreeKassa second secret is not set. Incoming payment notifications cannot be verified."
)
except ValidationError as e: except ValidationError as e:
logging.critical( logging.critical(
+4
View File
@@ -34,6 +34,10 @@
"pay_with_cryptopay_button": "💎 CryptoBot", "pay_with_cryptopay_button": "💎 CryptoBot",
"pay_with_tribute_button": "❤️ Tribute", "pay_with_tribute_button": "❤️ Tribute",
"pay_with_stars_button": "🌟 Telegram Stars", "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", "connect_button": "🔗 Connect",
"cancel_button": "❌ Cancel", "cancel_button": "❌ Cancel",
"payment_description_subscription": "Subscription payment for {months} mo.", "payment_description_subscription": "Subscription payment for {months} mo.",
+4
View File
@@ -34,6 +34,10 @@
"pay_with_cryptopay_button": "💎 CryptoBot", "pay_with_cryptopay_button": "💎 CryptoBot",
"pay_with_tribute_button": "❤️ Tribute", "pay_with_tribute_button": "❤️ Tribute",
"pay_with_stars_button": "🌟 Звезды Telegram", "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": "🔗 Подключиться", "connect_button": "🔗 Подключиться",
"cancel_button": "❌ Отмена", "cancel_button": "❌ Отмена",
"payment_description_subscription": "Оплата подписки на {months} мес.", "payment_description_subscription": "Оплата подписки на {months} мес.",
Binary file not shown.