v0.1
This commit is contained in:
@@ -32,6 +32,14 @@ YOOKASSA_DEFAULT_RECEIPT_EMAIL=your_email@example.com #
|
||||
YOOKASSA_VAT_CODE=1 # VAT code
|
||||
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
|
||||
CRYPTOPAY_TOKEN= # API token for CryptoPay
|
||||
CRYPTOPAY_NETWORK=mainnet # Network (mainnet or testnet)
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
- **Пробная подписка:** Система пробных подписок для новых пользователей (активируется вручную по кнопке).
|
||||
- **Промокоды:** Возможность применять промокоды для получения скидок или бонусных дней.
|
||||
- **Реферальная программа:** Пользователи могут приглашать друзей и получать за это бонусные дни подписки.
|
||||
- **Оплата:** Поддержка оплаты через YooKassa, FreeKassa, CryptoPay, Telegram Stars и Tribute.
|
||||
- **Оплата:** Поддержка оплаты через YooKassa, FreeKassa (REST API), CryptoPay, Telegram Stars и Tribute.
|
||||
|
||||
### Для администраторов:
|
||||
- **Защищенная админ-панель:** Доступ только для администраторов, указанных в `ADMIN_IDS`.
|
||||
@@ -83,10 +83,11 @@
|
||||
| `CRYPTOPAY_TOKEN` | Токен из вашего CryptoPay App. |
|
||||
| `FREEKASSA_ENABLED` | Включить/выключить FreeKassa (`true`/`false`). |
|
||||
| `FREEKASSA_MERCHANT_ID` | ID вашего магазина в FreeKassa. |
|
||||
| `FREEKASSA_FIRST_SECRET` | Секретное слово №1 для формирования ссылок оплаты. |
|
||||
| `FREEKASSA_SECOND_SECRET` | Секретное слово №2 для проверки уведомлений. |
|
||||
| `FREEKASSA_PAYMENT_URL` | (Опционально) Базовый URL платёжной формы FreeKassa. По умолчанию `https://pay.freekassa.ru/`. |
|
||||
| `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`). |
|
||||
</details>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
from aiogram import Router, F, types
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from typing import Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -308,7 +309,7 @@ async def pay_fk_callback_handler(
|
||||
|
||||
user_id = callback.from_user.id
|
||||
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 = {
|
||||
"user_id": user_id,
|
||||
@@ -339,40 +340,191 @@ async def pay_fk_callback_handler(
|
||||
pass
|
||||
return
|
||||
|
||||
payment_link = None
|
||||
try:
|
||||
payment_link = freekassa_service.build_payment_link(
|
||||
payment_db_id=payment_record.payment_id,
|
||||
user_id=user_id,
|
||||
months=months,
|
||||
amount=price_rub,
|
||||
)
|
||||
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 = 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)
|
||||
|
||||
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:
|
||||
await callback.message.edit_text(
|
||||
get_text(key="payment_link_message", months=months),
|
||||
reply_markup=get_payment_url_keyboard(payment_link, current_lang, i18n),
|
||||
disable_web_page_preview=False,
|
||||
await callback.message.answer(
|
||||
get_text("freekassa_choose_method"),
|
||||
reply_markup=method_keyboard.as_markup(),
|
||||
)
|
||||
except Exception as e_edit:
|
||||
logging.warning(f"FreeKassa: edit message failed ({e_edit}), sending new message.")
|
||||
try:
|
||||
await callback.message.answer(
|
||||
get_text(key="payment_link_message", months=months),
|
||||
reply_markup=get_payment_url_keyboard(payment_link, current_lang, i18n),
|
||||
disable_web_page_preview=False,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
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()
|
||||
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,
|
||||
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:
|
||||
await payment_dal.update_payment_status_by_db_id(
|
||||
session,
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import asyncio
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
from typing import Optional, Dict, Any
|
||||
from urllib.parse import urlencode
|
||||
from typing import Optional, Dict, Any, Tuple
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp import ClientSession, ClientTimeout, web
|
||||
from aiogram import Bot
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
@@ -36,68 +39,162 @@ class FreeKassaService:
|
||||
self.subscription_service = subscription_service
|
||||
self.referral_service = referral_service
|
||||
|
||||
self.merchant_id: Optional[str] = settings.FREEKASSA_MERCHANT_ID
|
||||
self.first_secret: Optional[str] = settings.FREEKASSA_FIRST_SECRET
|
||||
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.payment_url: str = settings.FREEKASSA_PAYMENT_URL.rstrip("/")
|
||||
self.currency: str = settings.FREEKASSA_CURRENCY.upper() if settings.FREEKASSA_CURRENCY else "RUB"
|
||||
self.default_currency: str = (
|
||||
settings.FREEKASSA_CURRENCY or settings.DEFAULT_CURRENCY_SYMBOL or "RUB"
|
||||
).upper()
|
||||
self.server_ip: Optional[str] = settings.FREEKASSA_PAYMENT_IP
|
||||
|
||||
self.configured: bool = bool(
|
||||
settings.FREEKASSA_ENABLED
|
||||
and self.merchant_id
|
||||
and self.first_secret
|
||||
and self.second_secret
|
||||
)
|
||||
self.api_base_url: str = "https://api.fk.life/v1"
|
||||
self._timeout = ClientTimeout(total=15)
|
||||
self._session: Optional[ClientSession] = None
|
||||
self._nonce_lock = asyncio.Lock()
|
||||
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:
|
||||
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
|
||||
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)
|
||||
return f"{quantized:.2f}"
|
||||
|
||||
def build_payment_link(
|
||||
async def create_order(
|
||||
self,
|
||||
*,
|
||||
payment_db_id: int,
|
||||
user_id: int,
|
||||
months: int,
|
||||
amount: float,
|
||||
currency: Optional[str],
|
||||
method_code: int,
|
||||
email: Optional[str] = None,
|
||||
ip_address: Optional[str] = None,
|
||||
extra_params: Optional[Dict[str, Any]] = None,
|
||||
) -> Optional[str]:
|
||||
) -> Tuple[bool, Dict[str, Any]]:
|
||||
if not self.configured:
|
||||
logging.error("FreeKassaService is not configured. Cannot build payment link.")
|
||||
return None
|
||||
logging.error("FreeKassaService is not configured. Cannot create order.")
|
||||
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)
|
||||
signature_source = f"{self.merchant_id}:{amount_str}:{self.first_secret}:{payment_db_id}"
|
||||
signature = hashlib.md5(signature_source.encode("utf-8")).hexdigest()
|
||||
currency_code = (currency or self.default_currency or "RUB").upper()
|
||||
|
||||
params: Dict[str, Any] = {
|
||||
"m": self.merchant_id,
|
||||
"oa": amount_str,
|
||||
"o": str(payment_db_id),
|
||||
"currency": self.currency,
|
||||
"s": signature,
|
||||
payload: Dict[str, Any] = {
|
||||
"shopId": int(self.shop_id),
|
||||
"nonce": await self._generate_nonce(),
|
||||
"paymentId": str(payment_db_id),
|
||||
"i": int(method_code),
|
||||
"amount": amount_str,
|
||||
"currency": currency_code,
|
||||
"email": email,
|
||||
"ip": ip_address,
|
||||
"us_user_id": str(user_id),
|
||||
"us_months": str(months),
|
||||
"us_payment_db_id": str(payment_db_id),
|
||||
}
|
||||
|
||||
if extra_params:
|
||||
for key, value in extra_params.items():
|
||||
if value is None:
|
||||
continue
|
||||
params[f"us_{key}"] = value
|
||||
payload[key] = value
|
||||
|
||||
query_string = urlencode(params, doseq=False, safe=":")
|
||||
return f"{self.payment_url}?{query_string}"
|
||||
payload["signature"] = self._sign_payload(payload)
|
||||
|
||||
def _validate_signature(self, merchant_order_id: str, amount: str, provided_signature: str) -> bool:
|
||||
if not self.configured:
|
||||
session = await self._get_session()
|
||||
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
|
||||
signature_source = f"{self.merchant_id}:{amount}:{self.second_secret}:{merchant_order_id}"
|
||||
expected_signature = hashlib.md5(signature_source.encode("utf-8")).hexdigest()
|
||||
return expected_signature.lower() == provided_signature.lower()
|
||||
|
||||
if self.shop_id and self.second_secret:
|
||||
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:
|
||||
if not self.configured:
|
||||
@@ -109,21 +206,29 @@ class FreeKassaService:
|
||||
logging.error(f"FreeKassa webhook: failed to read POST data: {e}")
|
||||
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:
|
||||
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:
|
||||
payload_dict = {}
|
||||
data = {}
|
||||
|
||||
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")
|
||||
if merchant_id != self.merchant_id:
|
||||
if merchant_id != self.shop_id:
|
||||
logging.error(f"FreeKassa webhook: merchant mismatch (got {merchant_id})")
|
||||
return web.Response(status=403, text="merchant_mismatch")
|
||||
|
||||
signature = _get("SIGN")
|
||||
signature = _get("SIGN") or _get("signature")
|
||||
if not signature:
|
||||
logging.error("FreeKassa webhook: 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")
|
||||
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")
|
||||
return web.Response(status=403, text="invalid_signature")
|
||||
|
||||
|
||||
+14
-8
@@ -50,6 +50,8 @@ class Settings(BaseSettings):
|
||||
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
|
||||
|
||||
YOOKASSA_ENABLED: bool = Field(default=True)
|
||||
STARS_ENABLED: bool = Field(default=True)
|
||||
@@ -386,14 +388,18 @@ def get_settings() -> Settings:
|
||||
logging.warning(
|
||||
"CRITICAL: YooKassa credentials (SHOP_ID or SECRET_KEY) are not set. Payments will not work."
|
||||
)
|
||||
if _settings_instance.FREEKASSA_ENABLED and (
|
||||
not _settings_instance.FREEKASSA_MERCHANT_ID
|
||||
or not _settings_instance.FREEKASSA_FIRST_SECRET
|
||||
or not _settings_instance.FREEKASSA_SECOND_SECRET
|
||||
):
|
||||
logging.warning(
|
||||
"CRITICAL: FreeKassa is enabled but credentials are incomplete (merchant ID or secret words). FreeKassa payments will not work."
|
||||
)
|
||||
if _settings_instance.FREEKASSA_ENABLED:
|
||||
if (
|
||||
not _settings_instance.FREEKASSA_MERCHANT_ID
|
||||
or not _settings_instance.FREEKASSA_API_KEY
|
||||
):
|
||||
logging.warning(
|
||||
"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:
|
||||
logging.critical(
|
||||
|
||||
@@ -34,6 +34,10 @@
|
||||
"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.",
|
||||
|
||||
@@ -34,6 +34,10 @@
|
||||
"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} мес.",
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user