feat: add heleket payment provider

This commit is contained in:
3252a8
2026-05-18 15:26:07 +03:00
parent 960754c54e
commit c9c12b9ed6
8 changed files with 614 additions and 3 deletions
+22 -2
View File
@@ -90,8 +90,9 @@ CRYPTOPAY_ENABLED=True #
PLATEGA_ENABLED=False # Turn on PLATEGA
SEVERPAY_ENABLED=False # Turn on SeverPay
WATA_ENABLED=False # Turn on Wata
# Order of payment methods (top to bottom). Supported: severpay, wata, freekassa, platega, yookassa, stars, cryptopay
PAYMENT_METHODS_ORDER=severpay,wata,yookassa,cryptopay,freekassa,platega,stars
HELEKET_ENABLED=False # Turn on Heleket (crypto payments)
# Order of payment methods (top to bottom). Supported: severpay, wata, freekassa, platega, yookassa, stars, cryptopay, heleket
PAYMENT_METHODS_ORDER=severpay,wata,yookassa,cryptopay,freekassa,platega,stars,heleket
# Payment button presentation overrides (all optional; empty = provider defaults)
# Text supports per-language overrides: *_LABEL_RU and *_LABEL_EN. Legacy *_LABEL applies to all languages if per-language values are empty.
@@ -144,6 +145,12 @@ PAYMENT_CRYPTOPAY_WEBAPP_ICON=Bitcoin # W
PAYMENT_CRYPTOPAY_TELEGRAM_LABEL_RU= # Telegram bot payment button text for CryptoPay (Russian)
PAYMENT_CRYPTOPAY_TELEGRAM_LABEL_EN= # Telegram bot payment button text for CryptoPay (English)
PAYMENT_CRYPTOPAY_TELEGRAM_EMOJI= # Telegram bot payment button emoji for CryptoPay
PAYMENT_HELEKET_WEBAPP_LABEL_RU= # WebApp payment button text for Heleket (Russian)
PAYMENT_HELEKET_WEBAPP_LABEL_EN= # WebApp payment button text for Heleket (English)
PAYMENT_HELEKET_WEBAPP_ICON=Bitcoin # WebApp payment button icon for Heleket
PAYMENT_HELEKET_TELEGRAM_LABEL_RU= # Telegram bot payment button text for Heleket (Russian)
PAYMENT_HELEKET_TELEGRAM_LABEL_EN= # Telegram bot payment button text for Heleket (English)
PAYMENT_HELEKET_TELEGRAM_EMOJI= # Telegram bot payment button emoji for Heleket
# YooKassa Payment Gateway Configuration
YOOKASSA_SHOP_ID=your_shop_id # Your store ID in YooKassa
@@ -203,6 +210,19 @@ WATA_WEBHOOK_VERIFY_SIGNATURE=True #
WATA_PUBLIC_KEY= # Optional: cached public key; leave empty to fetch from Wata
WATA_TRUSTED_IPS=62.84.126.140,51.250.106.150 # Wata webhook source IP allowlist
# Heleket Payment Gateway Configuration (https://doc.heleket.com)
HELEKET_BASE_URL=https://api.heleket.com # Base API URL
HELEKET_MERCHANT_ID= # Your merchant UUID from Heleket dashboard
HELEKET_API_KEY= # Payment API key from Heleket dashboard
HELEKET_CURRENCY=RUB # Invoice currency (fiat or crypto code, e.g. RUB, USD, USDT)
HELEKET_TO_CURRENCY= # Optional: target cryptocurrency for conversion (e.g. USDT)
HELEKET_NETWORK= # Optional: blockchain network code (e.g. tron, bsc, eth)
HELEKET_RETURN_URL= # Optional: redirect after invoice expires/cancel (defaults to bot link)
HELEKET_SUCCESS_URL= # Optional: redirect after successful payment (defaults to return URL)
HELEKET_LIFETIME_SECONDS=3600 # Invoice lifetime in seconds (300-43200)
HELEKET_VERIFY_WEBHOOK_SIGNATURE=True # Verify md5 signature on incoming webhook
HELEKET_TRUSTED_IPS=31.133.220.8 # Heleket webhook source IP allowlist
# Subscription Options. Specify cost parameters or payment links here.
1_MONTH_ENABLED=True
RUB_PRICE_1_MONTH=150
+482
View File
@@ -0,0 +1,482 @@
import base64
import hashlib
import hmac
import json
import logging
from typing import Any, Dict, Optional, Tuple
from aiogram import Bot, F, Router, types
from aiohttp import web
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import sessionmaker
from bot.middlewares.i18n import JsonI18n
from bot.services.referral_service import ReferralService
from bot.services.subscription_service import SubscriptionService
from bot.utils.request_security import ip_in_allowlist, request_client_ip
from config.settings import Settings
from db.dal import payment_dal
from .base import PaymentProviderSpec, ServiceFactoryContext, WebAppPaymentContext
from .shared import (
HttpClientMixin,
PaymentSuccessRequest,
build_payment_record_payload,
create_webapp_payment_record,
decimal_amounts_equal,
describe_payment,
finalize_successful_payment,
finalize_webapp_link_payment,
first_value,
format_decimal_amount,
lookup_payment_by_order_or_provider_id,
make_translator,
notify_callback_parse_error,
notify_payment_record_failure,
notify_service_unavailable,
notify_user_payment_failed,
parse_payment_callback,
payment_failed,
payment_unavailable,
render_link_or_fail,
)
router = Router(name="user_subscription_payments_heleket_router")
_LOG = "heleket"
_SUCCESS_STATUSES = {"paid", "paid_over"}
_FAILED_STATUSES = {"fail", "wrong_amount", "cancel", "system_fail"}
def _serialize_for_signature(payload: Dict[str, Any]) -> str:
"""Serialize JSON exactly the way Heleket signs it.
Heleket's PHP example uses ``json_encode`` with ``JSON_UNESCAPED_UNICODE``
and default forward-slash escaping. We replicate both: ``ensure_ascii=False``
keeps unicode untouched, then we manually escape ``/`` so that the base64
payload matches the one signed on the Heleket side.
"""
encoded = json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
return encoded.replace("/", "\\/")
def _compute_signature(payload: Dict[str, Any], api_key: str) -> str:
body = _serialize_for_signature(payload).encode("utf-8")
b64 = base64.b64encode(body).decode("ascii")
return hashlib.md5((b64 + api_key).encode("utf-8")).hexdigest()
class HeleketService(HttpClientMixin):
def __init__(
self,
*,
bot: Bot,
settings: Settings,
i18n: JsonI18n,
async_session_factory: sessionmaker,
subscription_service: SubscriptionService,
referral_service: ReferralService,
default_return_url: str,
):
self.bot = bot
self.settings = settings
self.i18n = i18n
self.async_session_factory = async_session_factory
self.subscription_service = subscription_service
self.referral_service = referral_service
self.base_url = (settings.HELEKET_BASE_URL or "https://api.heleket.com").rstrip("/")
self.merchant_id = settings.HELEKET_MERCHANT_ID or ""
self.api_key = settings.HELEKET_API_KEY or ""
self.currency = (settings.HELEKET_CURRENCY or "RUB").upper()
self.to_currency = (settings.HELEKET_TO_CURRENCY or "").strip() or None
self.network = (settings.HELEKET_NETWORK or "").strip() or None
self.return_url = settings.HELEKET_RETURN_URL or f"https://t.me/{default_return_url}"
self.success_url = settings.HELEKET_SUCCESS_URL or self.return_url
self.lifetime_seconds = settings.HELEKET_LIFETIME_SECONDS
self.verify_webhook_signature = settings.HELEKET_VERIFY_WEBHOOK_SIGNATURE
self._init_http_client(total_timeout=20)
self.configured: bool = bool(
settings.HELEKET_ENABLED and self.merchant_id and self.api_key
)
if not self.configured:
logging.warning(
"HeleketService initialized but not fully configured. Payments disabled."
)
async def create_payment_link(
self,
*,
payment_db_id: int,
amount: float,
currency: Optional[str],
description: str,
url_callback: Optional[str],
) -> Tuple[bool, Dict[str, Any]]:
if not self.configured:
logging.error("HeleketService is not configured. Cannot create payment link.")
return False, {"message": "service_not_configured"}
body: Dict[str, Any] = {
"amount": str(format_decimal_amount(amount)),
"currency": (currency or self.currency).upper(),
"order_id": str(payment_db_id),
"url_return": self.return_url,
"url_success": self.success_url,
"lifetime": int(self.lifetime_seconds),
}
if description:
body["additional_data"] = description[:255]
if self.to_currency:
body["to_currency"] = self.to_currency
if self.network:
body["network"] = self.network
if url_callback:
body["url_callback"] = url_callback
signed_body = _serialize_for_signature(body).encode("utf-8")
headers = {
"merchant": self.merchant_id,
"sign": _compute_signature(body, self.api_key),
"Content-Type": "application/json",
}
session = await self._get_session()
try:
async with session.post(
f"{self.base_url}/v1/payment",
data=signed_body,
headers=headers,
) as response:
response_text = await response.text()
try:
response_data = json.loads(response_text) if response_text else {}
except json.JSONDecodeError:
logging.error("Heleket create_payment_link: invalid JSON: %s", response_text)
return False, {
"status": response.status,
"message": "invalid_json",
"raw": response_text,
}
state = response_data.get("state") if isinstance(response_data, dict) else None
if response.status != 200 or state != 0:
logging.error(
"Heleket create_payment_link: API 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.exception("Heleket create_payment_link: request failed.")
return False, {"message": str(exc)}
def _verify_signature(self, payload: Dict[str, Any]) -> bool:
received = payload.get("sign")
if not isinstance(received, str) or not received:
return False
data = {k: v for k, v in payload.items() if k != "sign"}
expected = _compute_signature(data, self.api_key)
return hmac.compare_digest(expected, received)
async def webhook_route(self, request: web.Request) -> web.Response:
if not self.configured:
return web.Response(status=503, text="heleket_disabled")
client_ip = request_client_ip(request, trusted_proxies=self.settings.trusted_proxies)
if self.settings.heleket_trusted_ips and not ip_in_allowlist(
client_ip, self.settings.heleket_trusted_ips
):
logging.warning("Heleket webhook denied from unauthorized IP source.")
return web.Response(status=403, text="forbidden")
raw_body = await request.read()
try:
payload = json.loads(raw_body.decode("utf-8"))
except Exception:
logging.exception("Heleket webhook: failed to parse JSON.")
return web.Response(status=400, text="bad_request")
if not isinstance(payload, dict):
return web.Response(status=400, text="bad_request")
if self.verify_webhook_signature and not self._verify_signature(payload):
logging.warning("Heleket webhook: invalid signature.")
return web.Response(status=403, text="invalid_signature")
uuid_value = str(payload.get("uuid") or "").strip()
status = str(payload.get("status") or "").strip().lower()
order_id_raw = payload.get("order_id")
amount_raw = payload.get("amount")
currency = payload.get("currency") or self.currency
if not status or not (uuid_value or order_id_raw):
logging.error("Heleket webhook: missing status or ids: %s", payload)
return web.Response(status=400, text="missing_fields")
async with self.async_session_factory() as session:
payment = await lookup_payment_by_order_or_provider_id(
session,
order_id_raw=order_id_raw,
provider_payment_id=uuid_value or None,
)
if not payment:
logging.error(
"Heleket webhook: payment not found (order_id=%s, uuid=%s)",
order_id_raw,
uuid_value,
)
return web.Response(status=404, text="payment_not_found")
if payment.status == "succeeded" and status in _SUCCESS_STATUSES:
return web.Response(text="ok")
resolved_id = uuid_value or str(payment.payment_id)
if status in _SUCCESS_STATUSES:
if amount_raw is not None:
try:
if not decimal_amounts_equal(amount_raw, payment.amount):
logging.warning(
"Heleket webhook: amount mismatch for payment %s "
"(expected %s, got %s)",
payment.payment_id,
format_decimal_amount(payment.amount),
format_decimal_amount(amount_raw),
)
except Exception as exc:
logging.warning(
"Heleket webhook: failed to compare amounts for %s: %s",
payment.payment_id,
exc,
)
try:
await payment_dal.update_provider_payment_and_status(
session,
payment.payment_id,
resolved_id,
"succeeded",
)
await session.commit()
except Exception:
await session.rollback()
logging.exception(
"Heleket webhook: failed to mark payment %s as succeeded.",
resolved_id,
)
return web.Response(status=500, text="processing_error")
payment_units = payment.purchased_gb or payment.subscription_duration_months or 1
sale_mode = payment.sale_mode or (
"traffic" if self.settings.traffic_sale_mode else "subscription"
)
outcome = await finalize_successful_payment(
PaymentSuccessRequest(
bot=self.bot,
settings=self.settings,
i18n=self.i18n,
session=session,
subscription_service=self.subscription_service,
referral_service=self.referral_service,
payment=payment,
user_id=payment.user_id,
amount=float(payment.amount),
currency=str(currency),
sale_mode=sale_mode,
months=payment_units,
traffic_amount=float(payment_units),
provider_subscription="heleket",
provider_notification="heleket",
db_user=payment.user,
log_prefix="Heleket webhook",
)
)
if outcome is None:
return web.Response(status=500, text="processing_error")
return web.Response(text="ok")
if status in _FAILED_STATUSES:
try:
await payment_dal.update_provider_payment_and_status(
session,
payment.payment_id,
resolved_id,
"failed",
)
await session.commit()
except Exception:
await session.rollback()
logging.exception(
"Heleket webhook: failed to mark payment %s as failed.",
resolved_id,
)
return web.Response(status=500, text="processing_error")
await notify_user_payment_failed(
bot=self.bot,
settings=self.settings,
i18n=self.i18n,
session=session,
payment=payment,
)
return web.Response(text="ok")
logging.info(
"Heleket webhook: intermediate status '%s' for payment %s",
status,
resolved_id,
)
return web.Response(status=202, text="status_ignored")
@router.callback_query(F.data.startswith("pay_heleket:"))
async def pay_heleket_callback_handler(
callback: types.CallbackQuery,
settings: Settings,
i18n_data: dict,
heleket_service: HeleketService,
session: AsyncSession,
):
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
translator = make_translator(i18n, current_lang)
if not i18n or not callback.message:
await notify_callback_parse_error(callback, translator)
return
if not heleket_service or not heleket_service.configured:
logging.error("Heleket service is not configured or unavailable.")
await notify_service_unavailable(callback, translator)
return
parts = parse_payment_callback(callback.data or "")
if not parts:
logging.error("Invalid pay_heleket data in callback: %s", callback.data)
await notify_callback_parse_error(callback, translator)
return
currency_code = (heleket_service.currency or settings.DEFAULT_CURRENCY_SYMBOL or "RUB").upper()
payment_description = describe_payment(translator, parts)
record_payload = build_payment_record_payload(
user_id=callback.from_user.id,
amount=parts.price,
currency=currency_code,
status="pending_heleket",
description=payment_description,
months=parts.months,
provider="heleket",
sale_mode=parts.sale_mode,
)
try:
payment_record = await payment_dal.create_payment_record(session, record_payload)
await session.commit()
except Exception:
await session.rollback()
logging.exception(
"Heleket: failed to create payment record for user %s.", callback.from_user.id
)
await notify_payment_record_failure(callback, translator)
return
success, response_data = await heleket_service.create_payment_link(
payment_db_id=payment_record.payment_id,
amount=parts.price,
currency=currency_code,
description=payment_description,
url_callback=settings.heleket_full_webhook_url,
)
result = response_data.get("result") if isinstance(response_data, dict) else None
await render_link_or_fail(
callback,
translator=translator,
current_lang=current_lang,
i18n=i18n,
parts=parts,
session=session,
payment=payment_record,
api_success=success,
payment_url=first_value(result, "url"),
provider_payment_id=first_value(result, "uuid"),
log_prefix=_LOG,
)
async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
settings: Settings = ctx.request.app["settings"]
service: HeleketService = ctx.request.app["heleket_service"]
if not service or not service.configured:
return payment_unavailable()
currency = (service.currency or settings.DEFAULT_CURRENCY_SYMBOL or "RUB").upper()
try:
payment = await create_webapp_payment_record(
ctx,
amount=ctx.price,
currency=currency,
status="pending_heleket",
provider="heleket",
)
success, response_data = await service.create_payment_link(
payment_db_id=payment.payment_id,
amount=ctx.price,
currency=currency,
description=ctx.description,
url_callback=settings.heleket_full_webhook_url,
)
except Exception:
await ctx.session.rollback()
logging.exception("Heleket WebApp payment failed")
return payment_failed()
result = response_data.get("result") if isinstance(response_data, dict) else None
return await finalize_webapp_link_payment(
session=ctx.session,
payment=payment,
api_success=success,
payment_url=first_value(result, "url") if success else None,
provider_payment_id=first_value(result, "uuid"),
log_prefix="Heleket",
)
async def heleket_webhook_route(request: web.Request) -> web.Response:
service: HeleketService = request.app["heleket_service"]
return await service.webhook_route(request)
def create_service(ctx: ServiceFactoryContext) -> HeleketService:
return HeleketService(
bot=ctx.bot,
settings=ctx.settings,
i18n=ctx.i18n,
async_session_factory=ctx.async_session_factory,
subscription_service=ctx.subscription_service,
referral_service=ctx.referral_service,
default_return_url=ctx.bot_username_for_default_return,
)
SPEC = PaymentProviderSpec(
id="heleket",
provider_key="heleket",
label="Heleket",
webapp_label="Heleket",
webapp_labels={"ru": "Heleket", "en": "Heleket"},
webapp_icon="Bitcoin",
telegram_labels={"ru": "Heleket", "en": "Heleket"},
telegram_emoji="🪙",
pending_status="pending_heleket",
enabled=lambda settings: settings.HELEKET_ENABLED,
service_key="heleket_service",
button_text_key="pay_with_heleket_button",
callback_prefix="pay_heleket",
router=router,
create_service=create_service,
webhook_path=lambda settings: settings.heleket_webhook_path,
webhook_route=heleket_webhook_route,
create_webapp_payment=create_webapp_payment,
emoji="🪙",
)
+2 -1
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional
from . import cryptopay, freekassa, platega, severpay, stars, wata, yookassa
from . import cryptopay, freekassa, heleket, platega, severpay, stars, wata, yookassa
from .base import PaymentProviderPresentation, PaymentProviderSpec, ServiceFactoryContext
PAYMENT_PROVIDER_SPECS: tuple[PaymentProviderSpec, ...] = (
@@ -14,6 +14,7 @@ PAYMENT_PROVIDER_SPECS: tuple[PaymentProviderSpec, ...] = (
yookassa.SPEC,
stars.SPEC,
cryptopay.SPEC,
heleket.SPEC,
)
+102
View File
@@ -76,6 +76,18 @@ class PaymentSettings(BaseModel):
cryptopay_network: str
cryptopay_currency_type: str
cryptopay_asset: str
heleket_enabled: bool
heleket_merchant_id: Optional[str]
heleket_api_key: Optional[str]
heleket_base_url: str
heleket_currency: str
heleket_to_currency: Optional[str]
heleket_network: Optional[str]
heleket_return_url: Optional[str]
heleket_success_url: Optional[str]
heleket_lifetime_seconds: int
heleket_verify_webhook_signature: bool
heleket_trusted_ips: List[str]
class EmailSettings(BaseModel):
@@ -283,6 +295,34 @@ class Settings(BaseSettings):
description="Comma-separated Wata webhook IP allowlist.",
)
HELEKET_ENABLED: bool = Field(default=False)
HELEKET_MERCHANT_ID: Optional[str] = None
HELEKET_API_KEY: Optional[str] = None
HELEKET_BASE_URL: str = Field(default="https://api.heleket.com")
HELEKET_CURRENCY: str = Field(
default="RUB",
description="Invoice currency (fiat or crypto code).",
)
HELEKET_TO_CURRENCY: Optional[str] = Field(
default=None,
description="Optional target cryptocurrency for conversion (e.g. USDT).",
)
HELEKET_NETWORK: Optional[str] = Field(
default=None,
description="Optional blockchain network code (e.g. tron, bsc).",
)
HELEKET_RETURN_URL: Optional[str] = None
HELEKET_SUCCESS_URL: Optional[str] = None
HELEKET_LIFETIME_SECONDS: int = Field(
default=3600,
description="Invoice lifetime in seconds (300-43200).",
)
HELEKET_VERIFY_WEBHOOK_SIGNATURE: bool = Field(default=True)
HELEKET_TRUSTED_IPS: str = Field(
default="31.133.220.8",
description="Comma-separated Heleket webhook IP allowlist.",
)
YOOKASSA_ENABLED: bool = Field(default=True)
STARS_ENABLED: bool = Field(default=True)
PAYMENT_METHODS_ORDER: Optional[str] = Field(
@@ -337,6 +377,12 @@ class Settings(BaseSettings):
PAYMENT_CRYPTOPAY_TELEGRAM_LABEL_RU: Optional[str] = None
PAYMENT_CRYPTOPAY_TELEGRAM_LABEL_EN: Optional[str] = None
PAYMENT_CRYPTOPAY_TELEGRAM_EMOJI: Optional[str] = None
PAYMENT_HELEKET_WEBAPP_LABEL_RU: Optional[str] = None
PAYMENT_HELEKET_WEBAPP_LABEL_EN: Optional[str] = None
PAYMENT_HELEKET_WEBAPP_ICON: Optional[str] = None
PAYMENT_HELEKET_TELEGRAM_LABEL_RU: Optional[str] = None
PAYMENT_HELEKET_TELEGRAM_LABEL_EN: Optional[str] = None
PAYMENT_HELEKET_TELEGRAM_EMOJI: Optional[str] = None
MONTH_1_ENABLED: bool = Field(default=True, alias="1_MONTH_ENABLED")
MONTH_3_ENABLED: bool = Field(default=True, alias="3_MONTHS_ENABLED")
@@ -622,6 +668,18 @@ class Settings(BaseSettings):
cryptopay_network=self.CRYPTOPAY_NETWORK,
cryptopay_currency_type=self.CRYPTOPAY_CURRENCY_TYPE,
cryptopay_asset=self.CRYPTOPAY_ASSET,
heleket_enabled=self.HELEKET_ENABLED,
heleket_merchant_id=self.HELEKET_MERCHANT_ID,
heleket_api_key=self.HELEKET_API_KEY,
heleket_base_url=self.HELEKET_BASE_URL,
heleket_currency=self.HELEKET_CURRENCY,
heleket_to_currency=self.HELEKET_TO_CURRENCY,
heleket_network=self.HELEKET_NETWORK,
heleket_return_url=self.HELEKET_RETURN_URL,
heleket_success_url=self.HELEKET_SUCCESS_URL,
heleket_lifetime_seconds=self.HELEKET_LIFETIME_SECONDS,
heleket_verify_webhook_signature=self.HELEKET_VERIFY_WEBHOOK_SIGNATURE,
heleket_trusted_ips=self.heleket_trusted_ips,
)
@computed_field
@@ -822,6 +880,24 @@ class Settings(BaseSettings):
def wata_trusted_ips(self) -> List[str]:
return _split_csv(self.WATA_TRUSTED_IPS)
@computed_field
@property
def heleket_webhook_path(self) -> str:
return "/webhook/heleket"
@computed_field
@property
def heleket_full_webhook_url(self) -> Optional[str]:
base = self.WEBHOOK_BASE_URL
if base:
return f"{base.rstrip('/')}{self.heleket_webhook_path}"
return None
@computed_field
@property
def heleket_trusted_ips(self) -> List[str]:
return _split_csv(self.HELEKET_TRUSTED_IPS)
@computed_field
@property
def platega_webhook_path(self) -> str:
@@ -1055,6 +1131,7 @@ class Settings(BaseSettings):
"yookassa",
"stars",
"cryptopay",
"heleket",
]
if not self.PAYMENT_METHODS_ORDER:
return default_order
@@ -1168,6 +1245,12 @@ class Settings(BaseSettings):
"WATA_FAILED_URL",
"WATA_API_TOKEN",
"WATA_PUBLIC_KEY",
"HELEKET_MERCHANT_ID",
"HELEKET_API_KEY",
"HELEKET_RETURN_URL",
"HELEKET_SUCCESS_URL",
"HELEKET_TO_CURRENCY",
"HELEKET_NETWORK",
"CRYPT4_REDIRECT_URL",
"PRIVACY_POLICY_URL",
"USER_AGREEMENT_URL",
@@ -1210,6 +1293,17 @@ class Settings(BaseSettings):
return 3
return min(30, max(1, value))
@field_validator("HELEKET_LIFETIME_SECONDS", mode="before")
@classmethod
def validate_heleket_lifetime(cls, v):
if isinstance(v, str):
v = v.strip()
try:
value = int(v)
except (TypeError, ValueError):
return 3600
return min(43200, max(300, value))
# Notification types
LOG_NEW_USERS: bool = Field(
default=True, description="Send notifications for new user registrations"
@@ -1306,6 +1400,14 @@ def get_settings() -> Settings:
logging.warning(
"CRITICAL: Wata is enabled but WATA_API_TOKEN is missing. Wata payments will not work." # noqa: E501
)
if _settings_instance.HELEKET_ENABLED:
if (
not _settings_instance.HELEKET_MERCHANT_ID
or not _settings_instance.HELEKET_API_KEY
):
logging.warning(
"CRITICAL: Heleket is enabled but HELEKET_MERCHANT_ID or HELEKET_API_KEY is missing. Heleket payments will not work." # noqa: E501
)
except ValidationError as e:
logging.critical(f"Pydantic validation error while loading settings: {e}")
+1
View File
@@ -55,6 +55,7 @@
"pay_with_platega_crypto_button": "🪙 Pay with crypto",
"pay_with_severpay_button": "💳 SeverPay",
"pay_with_wata_button": "Wata",
"pay_with_heleket_button": "🪙 Heleket",
"back_to_payment_methods_button": "⬅️ Back",
"pay_with_cryptopay_button": "💎 CryptoBot",
"pay_with_stars_button": "🌟 Telegram Stars",
+1
View File
@@ -55,6 +55,7 @@
"pay_with_platega_crypto_button": "🪙 Оплата криптой",
"pay_with_severpay_button": "💳 SeverPay",
"pay_with_wata_button": "Wata",
"pay_with_heleket_button": "🪙 Heleket",
"back_to_payment_methods_button": "⬅️ Назад",
"pay_with_cryptopay_button": "💎 CryptoBot",
"pay_with_stars_button": "🌟 Звезды Telegram",
+1
View File
@@ -132,6 +132,7 @@ class BuildServicesWiringTests(unittest.TestCase):
"platega_service",
"severpay_service",
"wata_service",
"heleket_service",
}
self.assertEqual(set(services), expected_keys)
+3
View File
@@ -50,6 +50,7 @@ _PROVIDER_MODULES = {
"cryptopay": "CryptoPayService",
"stars": "StarsService",
"wata": "WataService",
"heleket": "HeleketService",
}
@@ -115,6 +116,7 @@ def test_service_keys_and_statuses_come_from_provider_specs():
"wata_service",
"stars_service",
"cryptopay_service",
"heleket_service",
}
assert set(pending_statuses()) >= {
"pending",
@@ -125,6 +127,7 @@ def test_service_keys_and_statuses_come_from_provider_specs():
"pending_wata",
"pending_cryptopay",
"pending_stars",
"pending_heleket",
}