Skip to content
This repository was archived by the owner on Nov 2, 2025. It is now read-only.

Commit e0ce367

Browse files
committed
Integrate YooMoney payment gateway
- YooMoney payment gateway integration - Add parameters for connecting the YooMoney payment method - Include YooMoney configuration instructions - Implement dynamic registration for all available payment gateways - Refactor the base class to simplify extension - Refactor redirect_to_connection - Unify the payment processing logic - Resolve issues with multiple button clicks
1 parent a877ecd commit e0ce367

File tree

22 files changed

+691
-474
lines changed

22 files changed

+691
-474
lines changed

README.md

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828

2929
**3X-UI-SHOP** is a comprehensive solution designed to automate the sale of VPN subscriptions through Telegram.
3030
The bot uses the **3X-UI** panel API for client management and supports multiple payment methods, including
31-
**~~Cryptomus~~**, **YooKassa**, and **Telegram Stars**.
31+
**~~Cryptomus~~**, **YooKassa**, **YooMoney**, and **Telegram Stars**.
3232

3333
The bot enables efficient subscription sales with advanced features:
3434

@@ -144,6 +144,7 @@ Before starting the installation, make sure you have the installed [**Docker**](
144144
| SHOP_PAYMENT_STARS_ENABLED || True | Enable Telegram stars payment |
145145
| ~~SHOP_PAYMENT_CRYPTOMUS_ENABLED~~ || ~~False~~ | ~~Enable Cryptomus payment~~ |
146146
| SHOP_PAYMENT_YOOKASSA_ENABLED || False | Enable Yookassa payment |
147+
| SHOP_PAYMENT_YOOMONEY_ENABLED || False | Enable Yoomoney payment |
147148
| | | |
148149
| XUI_USERNAME | 🔴 | - | Username for authentication in the 3X-UI panel |
149150
| XUI_PASSWORD | 🔴 | - | Password for authentication in the 3X-UI panel |
@@ -154,6 +155,9 @@ Before starting the installation, make sure you have the installed [**Docker**](
154155
| YOOKASSA_TOKEN || - | Token for YooKassa payment |
155156
| YOOKASSA_SHOP_ID || - | Shop ID for YooKassa payment |
156157
| | | |
158+
| YOOMONEY_WALLET_ID || - | Wallet ID for Yoomoney payment |
159+
| YOOMONEY_NOTIFICATION_SECRET || - | Notification secret key for Yoomoney payment |
160+
| | | |
157161
| REDIS_HOST || 3xui-shop-redis | Host of the Redis server |
158162
| REDIS_PORT || 6379 | Port of the Redis server |
159163
| REDIS_DB_NAME || 0 | Name of the Redis database |
@@ -217,25 +221,39 @@ Before starting the installation, make sure you have the installed [**Docker**](
217221

218222
2. **Environment Variables Setup:**
219223

220-
- Visit [API Tokens](https://dash.cloudflare.com/profile/api-tokens).
224+
- Visit the [API Tokens](https://dash.cloudflare.com/profile/api-tokens) page.
221225
- Click `View Global API Key` and set:
222-
- `CF_API_KEY`: Your Global API Key
226+
- `CF_API_KEY`: Your global API key
223227
- `CF_API_EMAIL`: Your Cloudflare email
224228

225229
### YooKassa Configuration
226230

227231
1. **Webhook Setup:**
228-
- Visit [HTTP Notifications](https://yookassa.ru/my/merchant/integration/http-notifications).
229-
- Specify the bot’s domain in the notification URL with `/yookassa` at the end (e.g., `https://3xui-shop.com/yookassa`).
232+
- Visit the [HTTP Notifications](https://yookassa.ru/my/merchant/integration/http-notifications) page.
233+
- Enter the bot’s domain in the notification URL, ending with `/yookassa` (e.g., `https://3xui-shop.com/yookassa`).
230234
- Select the following events:
231235
- `payment.succeeded`
232236
- `payment.waiting_for_capture`
233237
- `payment.canceled`
234238

235239
2. **Environment Variables Setup:**
236240
- Set the following environment variables:
237-
- `YOOKASSA_TOKEN`: Your Secret Key
238-
- `YOOKASSA_SHOP_ID`: Your Shop ID
241+
- `YOOKASSA_TOKEN`: Your secret key
242+
- `YOOKASSA_SHOP_ID`: Your shop ID
243+
244+
### YooMoney Configuration
245+
246+
1. **Webhook Setup:**
247+
- Visit the [HTTP Notifications](https://yoomoney.ru/transfer/myservices/http-notification) page.
248+
- Enter the bot’s domain in the notification URL, ending with `/yoomoney` (e.g., `https://3xui-shop.com/yoomoney`).
249+
- Copy the notification secret key.
250+
- Check the box for `sending HTTP-notifications`.
251+
- Save the changes.
252+
253+
2. **Environment Variables Setup:**
254+
- Set the following environment variables:
255+
- `YOOMONEY_WALLET_ID`: Your wallet ID
256+
- `YOOMONEY_NOTIFICATION_SECRET`: Your notification secret key
239257

240258
### 3X-UI Configuration
241259

@@ -263,6 +281,7 @@ A special thanks to the following individuals for their generous support:
263281
- **Boto**
264282
- [**@olshevskii-sergey**](https://github.com/olshevskii-sergey/)
265283
- **Aleksey**
284+
- [**@DmitryKryloff**](https://t.me/DmitryKryloff)
266285
267286
You can support me via the following methods:
268287

app/__main__.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from aiohttp.web import Application, _run_app
1313

1414
from app import logger
15-
from app.bot import filters, middlewares, routers, services
15+
from app.bot import filters, middlewares, routers, services, tasks
1616
from app.bot.middlewares import MaintenanceMiddleware
1717
from app.bot.models import ServicesContainer
1818
from app.bot.payment_gateways import GatewayFactory
@@ -37,7 +37,7 @@ async def on_shutdown(db: Database, bot: Bot, services: ServicesContainer) -> No
3737
logging.info("Bot stopped.")
3838

3939

40-
async def on_startup(config: Config, bot: Bot, services: ServicesContainer) -> None:
40+
async def on_startup(config: Config, bot: Bot, services: ServicesContainer, db: Database) -> None:
4141
webhook_url = urljoin(config.bot.DOMAIN, TELEGRAM_WEBHOOK)
4242

4343
if await bot.get_webhook_info() != webhook_url:
@@ -49,6 +49,8 @@ async def on_startup(config: Config, bot: Bot, services: ServicesContainer) -> N
4949
await services.notification.notify_developer(BOT_STARTED_TAG)
5050
logging.info("Bot started.")
5151

52+
tasks.transactions.start_scheduler(db.session)
53+
5254

5355
async def main() -> None:
5456
# Create web application
@@ -110,7 +112,7 @@ async def main() -> None:
110112
dispatcher.shutdown.register(on_shutdown)
111113

112114
# Enable Maintenance mode for developing # WARNING: remove before production
113-
# MaintenanceMiddleware.set_mode(True)
115+
MaintenanceMiddleware.set_mode(True)
114116

115117
# Register middlewares
116118
middlewares.register(dispatcher=dispatcher, i18n=i18n, session=db.session)

app/bot/middlewares/throttling.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def __init__(
1515
self,
1616
*,
1717
default_key: str | None = "default",
18-
default_ttl: float = 0.5,
18+
default_ttl: float = 0.3,
1919
**ttl_map: dict[str, float],
2020
) -> None:
2121
if default_key:

app/bot/payment_gateways/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
from .gateway_factory import GatewayFactory
44
from .telegram_stars import TelegramStars
55
from .yookassa import Yookassa
6+
from .yoomoney import Yoomoney

app/bot/payment_gateways/_gateway.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,30 @@
1+
import logging
12
from abc import ABC, abstractmethod
23

4+
import requests
5+
from aiogram import Bot
6+
from aiogram.fsm.storage.redis import RedisStorage
7+
from aiogram.utils.i18n import I18n
8+
from aiogram.utils.i18n import gettext as _
9+
from aiogram.utils.i18n import lazy_gettext as __
10+
from aiohttp.web import Application
11+
from sqlalchemy.ext.asyncio import async_sessionmaker
12+
13+
from app.bot.models import ServicesContainer, SubscriptionData
14+
from app.bot.routers.main_menu.handler import redirect_to_main_menu
15+
from app.bot.utils.constants import (
16+
DEFAULT_LANGUAGE,
17+
EVENT_PAYMENT_CANCELED_TAG,
18+
EVENT_PAYMENT_SUCCEEDED_TAG,
19+
Currency,
20+
TransactionStatus,
21+
)
22+
from app.bot.utils.formatting import format_device_count, format_subscription_period
23+
from app.config import Config
24+
from app.db.models import Transaction, User
25+
26+
logger = logging.getLogger(__name__)
27+
328
from app.bot.models import SubscriptionData
429
from app.bot.utils.constants import Currency
530

@@ -9,6 +34,24 @@ class PaymentGateway(ABC):
934
currency: Currency
1035
callback: str
1136

37+
def __init__(
38+
self,
39+
app: Application,
40+
config: Config,
41+
session: async_sessionmaker,
42+
storage: RedisStorage,
43+
bot: Bot,
44+
i18n: I18n,
45+
services: ServicesContainer,
46+
):
47+
self.app = app
48+
self.config = config
49+
self.session = session
50+
self.storage = storage
51+
self.bot = bot
52+
self.i18n = i18n
53+
self.services = services
54+
1255
@abstractmethod
1356
async def create_payment(self, data: SubscriptionData) -> str:
1457
pass
@@ -20,3 +63,91 @@ async def handle_payment_succeeded(self, payment_id: str) -> None:
2063
@abstractmethod
2164
async def handle_payment_canceled(self, payment_id: str) -> None:
2265
pass
66+
67+
async def _on_payment_succeeded(self, payment_id: str) -> None:
68+
logger.info(f"Payment succeeded {payment_id}")
69+
70+
async with self.session() as session:
71+
transaction = await Transaction.get_by_id(session=session, payment_id=payment_id)
72+
data = SubscriptionData.unpack(transaction.subscription)
73+
logger.debug(f"Subscription data unpacked: {data}")
74+
user = await User.get(session=session, tg_id=data.user_id)
75+
76+
await Transaction.update(
77+
session=session,
78+
payment_id=payment_id,
79+
status=TransactionStatus.COMPLETED,
80+
)
81+
82+
await self.services.notification.notify_developer(
83+
text=EVENT_PAYMENT_SUCCEEDED_TAG
84+
+ "\n\n"
85+
+ _("payment:event:payment_succeeded").format(
86+
payment_id=payment_id,
87+
user_id=user.tg_id,
88+
devices=format_device_count(data.devices),
89+
duration=format_subscription_period(data.duration),
90+
),
91+
)
92+
93+
locale = user.language_code if user else DEFAULT_LANGUAGE
94+
with self.i18n.use_locale(locale):
95+
await redirect_to_main_menu(bot=self.bot, user=user, storage=self.storage)
96+
97+
if data.is_extend:
98+
await self.services.vpn.extend_subscription(
99+
user=user,
100+
devices=data.devices,
101+
duration=data.duration,
102+
)
103+
logger.info(f"Subscription extended for user {user.tg_id}")
104+
await self.services.notification.notify_extend_success(
105+
user_id=user.tg_id,
106+
data=data,
107+
)
108+
elif data.is_change:
109+
await self.services.vpn.change_subscription(
110+
user=user,
111+
devices=data.devices,
112+
duration=data.duration,
113+
)
114+
logger.info(f"Subscription changed for user {user.tg_id}")
115+
await self.services.notification.notify_change_success(
116+
user_id=user.tg_id,
117+
data=data,
118+
)
119+
else:
120+
await self.services.vpn.create_subscription(
121+
user=user,
122+
devices=data.devices,
123+
duration=data.duration,
124+
)
125+
logger.info(f"Subscription created for user {user.tg_id}")
126+
key = await self.services.vpn.get_key(user)
127+
await self.services.notification.notify_purchase_success(
128+
user_id=user.tg_id,
129+
key=key,
130+
)
131+
132+
async def _on_payment_canceled(self, payment_id: str) -> None:
133+
logger.info(f"Payment canceled {payment_id}")
134+
async with self.session() as session:
135+
transaction = await Transaction.get_by_id(session=session, payment_id=payment_id)
136+
data = SubscriptionData.unpack(transaction.subscription)
137+
138+
await Transaction.update(
139+
session=session,
140+
payment_id=payment_id,
141+
status=TransactionStatus.CANCELED,
142+
)
143+
144+
await self.services.notification.notify_developer(
145+
text=EVENT_PAYMENT_CANCELED_TAG
146+
+ "\n\n"
147+
+ _("payment:event:payment_canceled").format(
148+
payment_id=payment_id,
149+
user_id=data.user_id,
150+
devices=format_device_count(data.devices),
151+
duration=format_subscription_period(data.duration),
152+
),
153+
)
Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import logging
22

33
from aiogram import Bot
4+
from aiogram.fsm.storage.redis import RedisStorage
5+
from aiogram.utils.i18n import I18n
46
from aiogram.utils.i18n import lazy_gettext as __
7+
from aiohttp.web import Application, Request, Response
8+
from sqlalchemy.ext.asyncio import async_sessionmaker
59

6-
from app.bot.models import SubscriptionData
10+
from app.bot.models import ServicesContainer, SubscriptionData
711
from app.bot.payment_gateways import PaymentGateway
8-
from app.bot.utils.constants import Currency
12+
from app.bot.utils.constants import CRYPTOMUS_WEBHOOK, Currency
913
from app.bot.utils.navigation import NavSubscription
1014
from app.config import Config
1115

@@ -17,20 +21,37 @@ class Cryptomus(PaymentGateway):
1721
currency = Currency.USD
1822
callback = NavSubscription.PAY_CRYPTOMUS
1923

20-
def __init__(self, config: Config, bot: Bot) -> None:
24+
def __init__(
25+
self,
26+
app: Application,
27+
config: Config,
28+
session: async_sessionmaker,
29+
storage: RedisStorage,
30+
bot: Bot,
31+
i18n: I18n,
32+
services: ServicesContainer,
33+
):
2134
self.name = __("payment:gateway:cryptomus")
35+
self.app = app
2236
self.config = config
37+
self.session = session
38+
self.storage = storage
2339
self.bot = bot
40+
self.i18n = i18n
41+
self.services = services
42+
43+
self.app.router.add_post(CRYPTOMUS_WEBHOOK, lambda request: self.webhook_handler(request))
2444
logger.info("Cryptomus payment gateway initialized.")
2545

2646
async def create_payment(self, data: SubscriptionData) -> str:
2747
logger.info(f"Payment link created for user {data.user_id}: {pay_url}")
2848
pass
2949

3050
async def handle_payment_succeeded(self, payment_id: str) -> None:
31-
logger.info(f"Payment succeeded {payment_id}")
32-
pass
51+
await self._on_payment_succeeded(payment_id)
3352

3453
async def handle_payment_canceled(self, payment_id: str) -> None:
35-
logger.info(f"Payment canceled {payment_id}")
54+
await self._on_payment_canceled(payment_id)
55+
56+
async def webhook_handler(self, request: Request) -> Response:
3657
pass

app/bot/payment_gateways/gateway_factory.py

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
from app.config import Config
99

1010
from ._gateway import PaymentGateway
11+
from .cryptomus import Cryptomus
1112
from .telegram_stars import TelegramStars
1213
from .yookassa import Yookassa
14+
from .yoomoney import Yoomoney
1315

1416

1517
class GatewayFactory:
@@ -38,25 +40,15 @@ def register_gateways(
3840
i18n: I18n,
3941
services: ServicesContainer,
4042
) -> None:
41-
if config.shop.PAYMENT_STARS_ENABLED:
42-
self.register_gateway(
43-
TelegramStars(
44-
config=config,
45-
session=session,
46-
bot=bot,
47-
services=services,
48-
)
49-
)
50-
51-
if config.shop.PAYMENT_YOOKASSA_ENABLED:
52-
self.register_gateway(
53-
Yookassa(
54-
app=app,
55-
config=config,
56-
session=session,
57-
storage=storage,
58-
bot=bot,
59-
i18n=i18n,
60-
services=services,
61-
)
62-
)
43+
dependencies = [app, config, session, storage, bot, i18n, services]
44+
45+
gateways = [
46+
(config.shop.PAYMENT_STARS_ENABLED, TelegramStars),
47+
(config.shop.PAYMENT_YOOKASSA_ENABLED, Yookassa),
48+
(config.shop.PAYMENT_YOOMONEY_ENABLED, Yoomoney),
49+
(config.shop.PAYMENT_CRYPTOMUS_ENABLED, Cryptomus),
50+
]
51+
52+
for enabled, gateway_cls in gateways:
53+
if enabled:
54+
self.register_gateway(gateway_cls(*dependencies))

0 commit comments

Comments
 (0)