diff --git a/.env.example b/.env.example
index 6fb22015..a18ea009 100644
--- a/.env.example
+++ b/.env.example
@@ -261,6 +261,22 @@ CRYPTOBOT_DEFAULT_ASSET=USDT
CRYPTOBOT_ASSETS=USDT,TON,BTC,ETH,LTC,BNB,TRX,USDC
CRYPTOBOT_INVOICE_EXPIRES_HOURS=24
+# HELEKET
+HELEKET_ENABLED=false
+HELEKET_MERCHANT_ID=
+HELEKET_API_KEY=
+HELEKET_BASE_URL=https://api.heleket.com/v1
+HELEKET_DEFAULT_CURRENCY=USDT
+HELEKET_DEFAULT_NETWORK=
+HELEKET_INVOICE_LIFETIME=3600
+HELEKET_MARKUP_PERCENT=0
+HELEKET_WEBHOOK_PATH=/heleket-webhook
+HELEKET_WEBHOOK_HOST=0.0.0.0
+HELEKET_WEBHOOK_PORT=8086
+HELEKET_CALLBACK_URL=
+HELEKET_RETURN_URL=
+HELEKET_SUCCESS_URL=
+
# MULENPAY
MULENPAY_ENABLED=false
MULENPAY_API_KEY=
diff --git a/README.md b/README.md
index 4e685ff2..9a524d80 100644
--- a/README.md
+++ b/README.md
@@ -35,7 +35,7 @@
### ⚡ **Полная автоматизация VPN бизнеса**
- 🎯 **Готовое решение** - разверни за 5 минут, начни продавать сегодня
-- 💰 **Многоканальные платежи** - Telegram Stars + Tribute + CryptoBot + YooKassa (СБП + карты) + MulenPay + PayPalych (СБП + карты) + WATA
+- 💰 **Многоканальные платежи** - Telegram Stars + Tribute + CryptoBot + Heleket + YooKassa (СБП + карты) + MulenPay + PayPalych (СБП + карты) + WATA
- 🔄 **Автоматизация 99%** - от регистрации до продления подписок
- - 📱 **MiniApp лк** - личный кабинет с возможностью покупки/продления подписки
- 📊 **Детальная аналитика** - полная картина вашего бизнеса
@@ -605,6 +605,13 @@ CRYPTOBOT_ENABLED=false
CRYPTOBOT_API_TOKEN=
CRYPTOBOT_WEBHOOK_PATH=/cryptobot-webhook
+# Heleket
+HELEKET_ENABLED=false
+HELEKET_MERCHANT_ID=
+HELEKET_API_KEY=
+HELEKET_WEBHOOK_PATH=/heleket-webhook
+HELEKET_WEBHOOK_PORT=8086
+
# MulenPay
MULENPAY_ENABLED=false
MULENPAY_API_KEY=
@@ -710,6 +717,7 @@ LOG_FILE=logs/bot.log
- 💳 Tribute
- 💳 YooKassa (СБП + банковские карты)
- 💰 CryptoBot (USDT, TON, BTC, ETH и др.)
+- 🪙 Heleket (криптовалюта с наценкой)
- 💳 MulenPay (СБП)
- 🏦 PayPalych/Pal24 (СБП + карты)
- 💳 **WATA**
@@ -907,6 +915,7 @@ LOG_FILE=logs/bot.log
- **Tribute**: Настрой webhook на `https://your-domain.com/tribute-webhook`
- **YooKassa**: Настрой webhook на `https://your-domain.com/yookassa-webhook`
- **CryptoBot**: Настрой webhook на `https://your-domain.com/cryptobot-webhook`
+ - **Heleket**: Настрой webhook на `https://your-domain.com/heleket-webhook`
- **MulenPay**: Настрой webhook на `https://your-domain.com/mulenpay-webhook`
- **PayPalych**: Укажи Result URL `https://your-domain.com/pal24-webhook` в кабинете Pal24
- **WATA**: Настрой webhook на `https://your-domain.com/wata-webhook`
@@ -1310,7 +1319,7 @@ docker stats
| Метрика | Значение |
|---------|----------|
-| 💳 **Платёжных систем** | 7 (Stars, YooKassa, Tribute, CryptoBot, MulenPay, Pal24, WATA) |
+| 💳 **Платёжных систем** | 8 (Stars, YooKassa, Tribute, CryptoBot, Heleket, MulenPay, Pal24, WATA) |
| 🌍 **Языков интерфейса** | 2 (RU, EN) с возможностью расширения |
| 📊 **Периодов подписки** | 6 (от 14 дней до года) |
| 🎁 **Типов промо-акций** | 5 (коды, группы, предложения, скидки, кампании) |
diff --git a/app/handlers/balance/heleket.py b/app/handlers/balance/heleket.py
index 22afc319..ea98858d 100644
--- a/app/handlers/balance/heleket.py
+++ b/app/handlers/balance/heleket.py
@@ -31,15 +31,11 @@ async def start_heleket_payment(
markup = settings.get_heleket_markup_percent()
markup_text: Optional[str]
if markup > 0:
- markup_text = texts.t(
- "PAYMENT_HELEKET_MARKUP",
- f"Наценка провайдера: {markup:.0f}%", # fallback
- )
+ label = texts.t("PAYMENT_HELEKET_MARKUP_LABEL", "Наценка провайдера")
+ markup_text = f"{label}: {markup:.0f}%"
elif markup < 0:
- markup_text = texts.t(
- "PAYMENT_HELEKET_DISCOUNT",
- f"Скидка провайдера: {abs(markup):.0f}%",
- )
+ label = texts.t("PAYMENT_HELEKET_DISCOUNT_LABEL", "Скидка провайдера")
+ markup_text = f"{label}: {abs(markup):.0f}%"
else:
markup_text = None
@@ -144,8 +140,13 @@ async def process_heleket_payment_amount(
markup_percent = None
if markup_percent:
- sign = "+" if markup_percent > 0 else ""
- details.append(f"📈 Наценка: {sign}{markup_percent}%")
+ label_markup = texts.t("PAYMENT_HELEKET_MARKUP_LABEL", "Наценка провайдера")
+ label_discount = texts.t("PAYMENT_HELEKET_DISCOUNT_LABEL", "Скидка провайдера")
+ absolute = abs(markup_percent)
+ if markup_percent > 0:
+ details.append(f"📈 {label_markup}: +{absolute}%")
+ else:
+ details.append(f"📉 {label_discount}: {absolute}%")
if payer_amount and payer_currency:
try:
diff --git a/app/localization/locales/en.json b/app/localization/locales/en.json
index e5acdda3..27f01504 100644
--- a/app/localization/locales/en.json
+++ b/app/localization/locales/en.json
@@ -976,6 +976,7 @@
"PAYMENT_CARD_YOOKASSA": "💳 Bank card (YooKassa)",
"PAYMENT_CHARGE_ERROR": "⚠️ Failed to charge the payment",
"PAYMENT_CRYPTOBOT": "🪙 Cryptocurrency (CryptoBot)",
+ "PAYMENT_HELEKET": "🪙 Cryptocurrency (Heleket)",
"PAYMENT_METHODS_FOOTER": "Choose a top-up method:",
"PAYMENT_METHODS_ONLY_SUPPORT": "💳 Balance top-up methods\n\n⚠️ Automated payment methods are temporarily unavailable.\nContact support to top up your balance.\n\nChoose a top-up method:",
"PAYMENT_METHODS_PROMPT": "Choose the payment method that suits you:",
@@ -983,6 +984,8 @@
"PAYMENT_METHODS_UNAVAILABLE_ALERT": "⚠️ Automated payment methods are temporarily unavailable. Contact support to top up your balance.",
"PAYMENT_METHOD_CRYPTOBOT_DESCRIPTION": "via CryptoBot",
"PAYMENT_METHOD_CRYPTOBOT_NAME": "🪙 Cryptocurrency",
+ "PAYMENT_METHOD_HELEKET_DESCRIPTION": "via Heleket",
+ "PAYMENT_METHOD_HELEKET_NAME": "🪙 Cryptocurrency (Heleket)",
"PAYMENT_METHOD_MULENPAY_DESCRIPTION": "via {mulenpay_name}",
"PAYMENT_METHOD_MULENPAY_NAME": "💳 Bank card ({mulenpay_name})",
"PAYMENT_METHOD_PAL24_DESCRIPTION": "via Faster Payments System",
@@ -999,6 +1002,8 @@
"PAYMENT_METHOD_YOOKASSA_NAME": "💳 Bank card",
"PAYMENT_METHOD_YOOKASSA_SBP_DESCRIPTION": "via YooKassa Fast Payment System",
"PAYMENT_METHOD_YOOKASSA_SBP_NAME": "🏦 SBP (YooKassa)",
+ "PAYMENT_HELEKET_MARKUP_LABEL": "Provider markup",
+ "PAYMENT_HELEKET_DISCOUNT_LABEL": "Provider discount",
"PAYMENT_RETURN_HOME_BUTTON": "🏠 Main menu",
"PAYMENT_SBP_YOOKASSA": "🏦 Pay via SBP (YooKassa)",
"PAYMENT_TELEGRAM_STARS": "⭐ Telegram Stars",
diff --git a/app/localization/locales/ru.json b/app/localization/locales/ru.json
index 627b1a8c..f2fb1a6d 100644
--- a/app/localization/locales/ru.json
+++ b/app/localization/locales/ru.json
@@ -976,6 +976,7 @@
"PAYMENT_CARD_YOOKASSA": "💳 Банковская карта (YooKassa)",
"PAYMENT_CHARGE_ERROR": "⚠️ Ошибка списания средств",
"PAYMENT_CRYPTOBOT": "🪙 Криптовалюта (CryptoBot)",
+ "PAYMENT_HELEKET": "🪙 Криптовалюта (Heleket)",
"PAYMENT_METHODS_FOOTER": "Выберите способ пополнения:",
"PAYMENT_METHODS_ONLY_SUPPORT": "💳 Способы пополнения баланса\n\n⚠️ В данный момент автоматические способы оплаты временно недоступны.\nОбратитесь в техподдержку для пополнения баланса.\n\nВыберите способ пополнения:",
"PAYMENT_METHODS_PROMPT": "Выберите удобный для вас способ оплаты:",
@@ -983,6 +984,8 @@
"PAYMENT_METHODS_UNAVAILABLE_ALERT": "⚠️ В данный момент автоматические способы оплаты временно недоступны. Для пополнения баланса обратитесь в техподдержку.",
"PAYMENT_METHOD_CRYPTOBOT_DESCRIPTION": "через CryptoBot",
"PAYMENT_METHOD_CRYPTOBOT_NAME": "🪙 Криптовалюта",
+ "PAYMENT_METHOD_HELEKET_DESCRIPTION": "через Heleket",
+ "PAYMENT_METHOD_HELEKET_NAME": "🪙 Криптовалюта (Heleket)",
"PAYMENT_METHOD_MULENPAY_DESCRIPTION": "через {mulenpay_name}",
"PAYMENT_METHOD_MULENPAY_NAME": "💳 Банковская карта ({mulenpay_name})",
"PAYMENT_METHOD_PAL24_DESCRIPTION": "через систему быстрых платежей",
@@ -999,6 +1002,8 @@
"PAYMENT_METHOD_YOOKASSA_NAME": "💳 Банковская карта",
"PAYMENT_METHOD_YOOKASSA_SBP_DESCRIPTION": "через систему быстрых платежей YooKassa",
"PAYMENT_METHOD_YOOKASSA_SBP_NAME": "🏦 СБП (YooKassa)",
+ "PAYMENT_HELEKET_MARKUP_LABEL": "Наценка провайдера",
+ "PAYMENT_HELEKET_DISCOUNT_LABEL": "Скидка провайдера",
"PAYMENT_RETURN_HOME_BUTTON": "🏠 На главную",
"PAYMENT_SBP_YOOKASSA": "🏬 Оплатить по СБП (YooKassa)",
"PAYMENT_TELEGRAM_STARS": "⭐ Telegram Stars",
diff --git a/docker-compose.local.yml b/docker-compose.local.yml
index d7849b7a..5a4444c4 100644
--- a/docker-compose.local.yml
+++ b/docker-compose.local.yml
@@ -75,6 +75,10 @@ services:
- "${WEB_API_PORT:-8080}:8080"
- "${TRIBUTE_WEBHOOK_PORT:-8081}:8081"
- "${YOOKASSA_WEBHOOK_PORT:-8082}:8082"
+ - "${CRYPTOBOT_WEBHOOK_PORT:-8083}:8083"
+ - "${PAL24_WEBHOOK_PORT:-8084}:8084"
+ - "${WATA_WEBHOOK_PORT:-8085}:8085"
+ - "${HELEKET_WEBHOOK_PORT:-8086}:8086"
networks:
- bot_network
healthcheck:
diff --git a/docker-compose.yml b/docker-compose.yml
index 4fb43f64..5a4444c4 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -78,6 +78,7 @@ services:
- "${CRYPTOBOT_WEBHOOK_PORT:-8083}:8083"
- "${PAL24_WEBHOOK_PORT:-8084}:8084"
- "${WATA_WEBHOOK_PORT:-8085}:8085"
+ - "${HELEKET_WEBHOOK_PORT:-8086}:8086"
networks:
- bot_network
healthcheck:
diff --git a/docs/project_structure_reference.md b/docs/project_structure_reference.md
index 4d20e7cb..4a464de2 100644
--- a/docs/project_structure_reference.md
+++ b/docs/project_structure_reference.md
@@ -74,6 +74,9 @@
- `app/database/crud/cryptobot.py` — Python-модуль
Классы: нет
Функции: нет
+- `app/database/crud/heleket.py` — Python-модуль
+ Классы: нет
+ Функции: нет
- `app/database/crud/discount_offer.py` — Python-модуль
Классы: нет
Функции: нет
@@ -158,6 +161,12 @@
- `app/external/cryptobot.py` — Python-модуль
Классы: `CryptoBotService` (2 методов)
Функции: нет
+- `app/external/heleket.py` — Python-модуль
+ Классы: `HeleketService` (3 методов)
+ Функции: нет
+- `app/external/heleket_webhook.py` — Python-модуль
+ Классы: `HeleketWebhookHandler` (3 методов)
+ Функции: `create_heleket_app`, `start_heleket_webhook_server`
- `app/external/pal24_client.py` — Async client for PayPalych (Pal24) API.
Классы: `Pal24APIError` — Base error for Pal24 API operations., `Pal24Response` (2 методов) — Wrapper for Pal24 API responses., `Pal24Client` (5 методов) — Async client implementing PayPalych API methods.
Функции: нет
@@ -726,6 +735,9 @@
- `tests/services/test_payment_service_cryptobot.py` — Тесты сценариев CryptoBot в PaymentService.
Классы: `DummySession` (2 методов), `DummyLocalPayment` (1 методов), `StubCryptoBotService` (1 методов)
Функции: `anyio_backend`, `_make_service`
+- `tests/services/test_payment_service_heleket.py` — Тесты сценариев Heleket в PaymentService.
+ Классы: `DummySession` (2 методов), `DummyLocalPayment` (1 методов), `StubHeleketService` (1 методов)
+ Функции: `anyio_backend`, `_make_service`
- `tests/services/test_payment_service_mulenpay.py` — Тесты для сценариев MulenPay в PaymentService.
Классы: `DummySession`, `DummyLocalPayment` (1 методов), `StubMulenPayService` (1 методов)
Функции: `anyio_backend`, `_make_service`
diff --git a/tests/services/test_payment_service_modularity.py b/tests/services/test_payment_service_modularity.py
index 0a20450e..af694020 100644
--- a/tests/services/test_payment_service_modularity.py
+++ b/tests/services/test_payment_service_modularity.py
@@ -11,6 +11,7 @@ if str(ROOT_DIR) not in sys.path:
from app.services.payment import ( # noqa: E402
CryptoBotPaymentMixin,
+ HeleketPaymentMixin,
MulenPayPaymentMixin,
Pal24PaymentMixin,
PaymentCommonMixin,
@@ -30,6 +31,7 @@ def test_payment_service_mro_contains_all_mixins() -> None:
YooKassaPaymentMixin,
TributePaymentMixin,
CryptoBotPaymentMixin,
+ HeleketPaymentMixin,
MulenPayPaymentMixin,
Pal24PaymentMixin,
WataPaymentMixin,
@@ -46,6 +48,7 @@ def test_payment_service_mro_contains_all_mixins() -> None:
"create_yookassa_payment",
"create_tribute_payment",
"create_cryptobot_payment",
+ "create_heleket_payment",
"create_mulenpay_payment",
"create_pal24_payment",
"create_wata_payment",
diff --git a/tests/services/test_payment_service_webhooks.py b/tests/services/test_payment_service_webhooks.py
index c47ef6a6..b33af870 100644
--- a/tests/services/test_payment_service_webhooks.py
+++ b/tests/services/test_payment_service_webhooks.py
@@ -2,6 +2,8 @@
from __future__ import annotations
+from __future__ import annotations
+
from pathlib import Path
from types import SimpleNamespace, ModuleType
from typing import Any, Dict
@@ -16,6 +18,7 @@ if str(ROOT_DIR) not in sys.path:
import app.services.payment_service as payment_service_module # noqa: E402
from app.services.payment_service import PaymentService # noqa: E402
+from app.database.models import PaymentMethod # noqa: E402
from app.config import settings # noqa: E402
@@ -256,6 +259,146 @@ async def test_process_cryptobot_webhook_success(monkeypatch: pytest.MonkeyPatch
assert admin_calls
+@pytest.mark.anyio("asyncio")
+async def test_process_heleket_webhook_success(monkeypatch: pytest.MonkeyPatch) -> None:
+ bot = DummyBot()
+ service = _make_service(bot)
+ fake_session = FakeSession()
+
+ payment = SimpleNamespace(
+ uuid="heleket-uuid",
+ order_id="heleket-order",
+ user_id=77,
+ amount="150.00",
+ amount_float=150.0,
+ amount_kopeks=15000,
+ status="check",
+ payer_amount=None,
+ payer_currency=None,
+ exchange_rate=None,
+ discount_percent=None,
+ payment_url=None,
+ transaction_id=None,
+ )
+
+ async def fake_get_by_uuid(db, uuid):
+ return payment if uuid == payment.uuid else None
+
+ async def fake_get_by_order(db, order_id):
+ return payment if order_id == payment.order_id else None
+
+ async def fake_update(
+ db,
+ uuid,
+ *,
+ status=None,
+ payer_amount=None,
+ payer_currency=None,
+ exchange_rate=None,
+ discount_percent=None,
+ paid_at=None,
+ payment_url=None,
+ metadata=None,
+ ):
+ if status is not None:
+ payment.status = status
+ if payer_amount is not None:
+ payment.payer_amount = payer_amount
+ if payer_currency is not None:
+ payment.payer_currency = payer_currency
+ if exchange_rate is not None:
+ payment.exchange_rate = exchange_rate
+ if discount_percent is not None:
+ payment.discount_percent = discount_percent
+ if payment_url is not None:
+ payment.payment_url = payment_url
+ payment.paid_at = paid_at
+ if metadata:
+ payment.metadata_json = metadata
+ return payment
+
+ async def fake_link(db, uuid, transaction_id):
+ payment.transaction_id = transaction_id
+ return payment
+
+ heleket_module = ModuleType("app.database.crud.heleket")
+ heleket_module.get_heleket_payment_by_uuid = fake_get_by_uuid
+ heleket_module.get_heleket_payment_by_order_id = fake_get_by_order
+ heleket_module.update_heleket_payment = fake_update
+ heleket_module.link_heleket_payment_to_transaction = fake_link
+ monkeypatch.setitem(sys.modules, "app.database.crud.heleket", heleket_module)
+
+ transactions: list[Dict[str, Any]] = []
+
+ async def fake_create_transaction(db, **kwargs):
+ transactions.append(kwargs)
+ return SimpleNamespace(id=321, **kwargs)
+
+ monkeypatch.setattr(payment_service_module, "create_transaction", fake_create_transaction)
+
+ user = SimpleNamespace(
+ id=77,
+ telegram_id=7700,
+ balance_kopeks=0,
+ has_made_first_topup=False,
+ promo_group=None,
+ subscription=None,
+ referred_by_id=None,
+ referrer=None,
+ language="ru",
+ )
+
+ async def fake_get_user(db, user_id):
+ return user if user_id == user.id else None
+
+ monkeypatch.setattr(payment_service_module, "get_user_by_id", fake_get_user)
+ monkeypatch.setattr("app.services.payment.heleket.format_referrer_info", lambda u: "")
+
+ monkeypatch.setattr(type(settings), "format_price", lambda self, amount: f"{amount / 100:.2f}₽", raising=False)
+
+ referral_stub = SimpleNamespace(process_referral_topup=AsyncMock())
+ monkeypatch.setitem(sys.modules, "app.services.referral_service", referral_stub)
+
+ admin_calls: list[Any] = []
+
+ class DummyAdminService:
+ def __init__(self, bot):
+ self.bot = bot
+
+ async def send_balance_topup_notification(self, *args, **kwargs):
+ admin_calls.append((args, kwargs))
+
+ monkeypatch.setitem(
+ sys.modules,
+ "app.services.admin_notification_service",
+ SimpleNamespace(AdminNotificationService=lambda bot: DummyAdminService(bot)),
+ )
+
+ service.build_topup_success_keyboard = AsyncMock(return_value=None)
+
+ payload = {
+ "uuid": "heleket-uuid",
+ "status": "paid",
+ "payer_amount": "2.50",
+ "payer_currency": "USDT",
+ "discount_percent": -5,
+ "payer_amount_exchange_rate": "0.0166",
+ "paid_at": "2024-01-02T12:00:00Z",
+ "url": "https://pay.example",
+ }
+
+ result = await service.process_heleket_webhook(fake_session, payload)
+
+ assert result is True
+ assert transactions and transactions[0]["payment_method"] == PaymentMethod.HELEKET
+ assert payment.transaction_id == 321
+ assert user.balance_kopeks == 15000
+ assert user.has_made_first_topup is True
+ assert fake_session.commits >= 1
+ assert bot.sent_messages
+ assert admin_calls
+ referral_stub.process_referral_topup.assert_awaited_once()
+
@pytest.mark.anyio("asyncio")
async def test_process_yookassa_webhook_success(monkeypatch: pytest.MonkeyPatch) -> None:
bot = DummyBot()