mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-22 04:12:09 +00:00
Merge pull request #1323 from Fr1ngg/q1598f-bedolaga/add-new-balance-topping-method-to-bot
Fix missing logging import in WATA CRUD helpers
This commit is contained in:
@@ -231,6 +231,19 @@ class Settings(BaseSettings):
|
||||
PAL24_SBP_BUTTON_VISIBLE: bool = True
|
||||
PAL24_CARD_BUTTON_VISIBLE: bool = True
|
||||
|
||||
WATA_ENABLED: bool = False
|
||||
WATA_BASE_URL: str = "https://api.wata.pro/api/h2h"
|
||||
WATA_ACCESS_TOKEN: Optional[str] = None
|
||||
WATA_TERMINAL_PUBLIC_ID: Optional[str] = None
|
||||
WATA_PAYMENT_DESCRIPTION: str = "Пополнение баланса"
|
||||
WATA_PAYMENT_TYPE: str = "OneTime"
|
||||
WATA_SUCCESS_REDIRECT_URL: Optional[str] = None
|
||||
WATA_FAIL_REDIRECT_URL: Optional[str] = None
|
||||
WATA_LINK_TTL_MINUTES: Optional[int] = None
|
||||
WATA_MIN_AMOUNT_KOPEKS: int = 10000
|
||||
WATA_MAX_AMOUNT_KOPEKS: int = 100000000
|
||||
WATA_REQUEST_TIMEOUT: int = 30
|
||||
|
||||
MAIN_MENU_MODE: str = "default"
|
||||
CONNECT_BUTTON_MODE: str = "guide"
|
||||
MINIAPP_CUSTOM_URL: str = ""
|
||||
@@ -737,6 +750,13 @@ class Settings(BaseSettings):
|
||||
and self.PAL24_SHOP_ID is not None
|
||||
)
|
||||
|
||||
def is_wata_enabled(self) -> bool:
|
||||
return (
|
||||
self.WATA_ENABLED
|
||||
and self.WATA_ACCESS_TOKEN is not None
|
||||
and self.WATA_TERMINAL_PUBLIC_ID is not None
|
||||
)
|
||||
|
||||
def get_cryptobot_base_url(self) -> str:
|
||||
if self.CRYPTOBOT_TESTNET:
|
||||
return "https://testnet-pay.crypt.bot"
|
||||
|
||||
168
app/database/crud/wata.py
Normal file
168
app/database/crud/wata.py
Normal file
@@ -0,0 +1,168 @@
|
||||
"""CRUD helpers for WATA payment records."""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database.models import WataPayment
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def create_wata_payment(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
user_id: int,
|
||||
payment_link_id: str,
|
||||
amount_kopeks: int,
|
||||
currency: str,
|
||||
description: Optional[str],
|
||||
status: str,
|
||||
type_: Optional[str],
|
||||
url: Optional[str],
|
||||
order_id: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
expires_at: Optional[datetime] = None,
|
||||
terminal_public_id: Optional[str] = None,
|
||||
success_redirect_url: Optional[str] = None,
|
||||
fail_redirect_url: Optional[str] = None,
|
||||
) -> WataPayment:
|
||||
payment = WataPayment(
|
||||
user_id=user_id,
|
||||
payment_link_id=payment_link_id,
|
||||
order_id=order_id,
|
||||
amount_kopeks=amount_kopeks,
|
||||
currency=currency,
|
||||
description=description,
|
||||
status=status,
|
||||
type=type_,
|
||||
url=url,
|
||||
metadata_json=metadata or {},
|
||||
expires_at=expires_at,
|
||||
terminal_public_id=terminal_public_id,
|
||||
success_redirect_url=success_redirect_url,
|
||||
fail_redirect_url=fail_redirect_url,
|
||||
)
|
||||
|
||||
db.add(payment)
|
||||
await db.commit()
|
||||
await db.refresh(payment)
|
||||
|
||||
logger.info(
|
||||
"Создан Wata платеж #%s для пользователя %s: %s копеек (статус %s)",
|
||||
payment.id,
|
||||
user_id,
|
||||
amount_kopeks,
|
||||
status,
|
||||
)
|
||||
|
||||
return payment
|
||||
|
||||
|
||||
async def get_wata_payment_by_id(
|
||||
db: AsyncSession,
|
||||
payment_id: int,
|
||||
) -> Optional[WataPayment]:
|
||||
result = await db.execute(
|
||||
select(WataPayment).where(WataPayment.id == payment_id)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def get_wata_payment_by_link_id(
|
||||
db: AsyncSession,
|
||||
payment_link_id: str,
|
||||
) -> Optional[WataPayment]:
|
||||
result = await db.execute(
|
||||
select(WataPayment).where(WataPayment.payment_link_id == payment_link_id)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def get_wata_payment_by_order_id(
|
||||
db: AsyncSession,
|
||||
order_id: str,
|
||||
) -> Optional[WataPayment]:
|
||||
result = await db.execute(
|
||||
select(WataPayment).where(WataPayment.order_id == order_id)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def update_wata_payment_status(
|
||||
db: AsyncSession,
|
||||
payment: WataPayment,
|
||||
*,
|
||||
status: Optional[str] = None,
|
||||
is_paid: Optional[bool] = None,
|
||||
paid_at: Optional[datetime] = None,
|
||||
last_status: Optional[str] = None,
|
||||
url: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
callback_payload: Optional[Dict[str, Any]] = None,
|
||||
terminal_public_id: Optional[str] = None,
|
||||
) -> WataPayment:
|
||||
update_values: Dict[str, Any] = {}
|
||||
|
||||
if status is not None:
|
||||
update_values["status"] = status
|
||||
if is_paid is not None:
|
||||
update_values["is_paid"] = is_paid
|
||||
if paid_at is not None:
|
||||
update_values["paid_at"] = paid_at
|
||||
if last_status is not None:
|
||||
update_values["last_status"] = last_status
|
||||
if url is not None:
|
||||
update_values["url"] = url
|
||||
if metadata is not None:
|
||||
update_values["metadata_json"] = metadata
|
||||
if callback_payload is not None:
|
||||
update_values["callback_payload"] = callback_payload
|
||||
if terminal_public_id is not None:
|
||||
update_values["terminal_public_id"] = terminal_public_id
|
||||
|
||||
if not update_values:
|
||||
return payment
|
||||
|
||||
await db.execute(
|
||||
update(WataPayment)
|
||||
.where(WataPayment.id == payment.id)
|
||||
.values(**update_values)
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(payment)
|
||||
|
||||
logger.info(
|
||||
"Обновлен Wata платеж %s: статус=%s, is_paid=%s",
|
||||
payment.payment_link_id,
|
||||
payment.status,
|
||||
payment.is_paid,
|
||||
)
|
||||
|
||||
return payment
|
||||
|
||||
|
||||
async def link_wata_payment_to_transaction(
|
||||
db: AsyncSession,
|
||||
payment: WataPayment,
|
||||
transaction_id: int,
|
||||
) -> WataPayment:
|
||||
await db.execute(
|
||||
update(WataPayment)
|
||||
.where(WataPayment.id == payment.id)
|
||||
.values(transaction_id=transaction_id)
|
||||
)
|
||||
await db.commit()
|
||||
await db.refresh(payment)
|
||||
|
||||
logger.info(
|
||||
"Wata платеж %s привязан к транзакции %s",
|
||||
payment.payment_link_id,
|
||||
transaction_id,
|
||||
)
|
||||
|
||||
return payment
|
||||
@@ -77,6 +77,7 @@ class PaymentMethod(Enum):
|
||||
CRYPTOBOT = "cryptobot"
|
||||
MULENPAY = "mulenpay"
|
||||
PAL24 = "pal24"
|
||||
WATA = "wata"
|
||||
MANUAL = "manual"
|
||||
|
||||
|
||||
@@ -293,6 +294,56 @@ class Pal24Payment(Base):
|
||||
)
|
||||
|
||||
|
||||
class WataPayment(Base):
|
||||
__tablename__ = "wata_payments"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
|
||||
payment_link_id = Column(String(64), unique=True, nullable=False, index=True)
|
||||
order_id = Column(String(255), nullable=True, index=True)
|
||||
amount_kopeks = Column(Integer, nullable=False)
|
||||
currency = Column(String(10), nullable=False, default="RUB")
|
||||
description = Column(Text, nullable=True)
|
||||
type = Column(String(50), nullable=True)
|
||||
|
||||
status = Column(String(50), nullable=False, default="Opened")
|
||||
is_paid = Column(Boolean, default=False)
|
||||
paid_at = Column(DateTime, nullable=True)
|
||||
last_status = Column(String(50), nullable=True)
|
||||
terminal_public_id = Column(String(64), nullable=True)
|
||||
|
||||
url = Column(Text, nullable=True)
|
||||
success_redirect_url = Column(Text, nullable=True)
|
||||
fail_redirect_url = Column(Text, nullable=True)
|
||||
metadata_json = Column(JSON, nullable=True)
|
||||
callback_payload = Column(JSON, nullable=True)
|
||||
|
||||
expires_at = Column(DateTime, nullable=True)
|
||||
|
||||
transaction_id = Column(Integer, ForeignKey("transactions.id"), nullable=True)
|
||||
|
||||
created_at = Column(DateTime, default=func.now())
|
||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
||||
|
||||
user = relationship("User", backref="wata_payments")
|
||||
transaction = relationship("Transaction", backref="wata_payment")
|
||||
|
||||
@property
|
||||
def amount_rubles(self) -> float:
|
||||
return self.amount_kopeks / 100
|
||||
|
||||
def __repr__(self) -> str: # pragma: no cover - debug helper
|
||||
return (
|
||||
"<WataPayment(id={0}, link_id={1}, amount={2}₽, status={3})>".format(
|
||||
self.id,
|
||||
self.payment_link_id,
|
||||
self.amount_rubles,
|
||||
self.status,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class PromoGroup(Base):
|
||||
__tablename__ = "promo_groups"
|
||||
|
||||
|
||||
@@ -706,6 +706,172 @@ async def create_pal24_payments_table():
|
||||
return False
|
||||
|
||||
|
||||
async def create_wata_payments_table():
|
||||
table_exists = await check_table_exists('wata_payments')
|
||||
if table_exists:
|
||||
logger.info("Таблица wata_payments уже существует")
|
||||
return True
|
||||
|
||||
try:
|
||||
async with engine.begin() as conn:
|
||||
db_type = await get_database_type()
|
||||
|
||||
if db_type == 'sqlite':
|
||||
create_sql = """
|
||||
CREATE TABLE wata_payments (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
payment_link_id VARCHAR(64) NOT NULL UNIQUE,
|
||||
order_id VARCHAR(255) NULL,
|
||||
amount_kopeks INTEGER NOT NULL,
|
||||
currency VARCHAR(10) NOT NULL DEFAULT 'RUB',
|
||||
description TEXT NULL,
|
||||
type VARCHAR(50) NULL,
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'Opened',
|
||||
is_paid BOOLEAN NOT NULL DEFAULT 0,
|
||||
paid_at DATETIME NULL,
|
||||
last_status VARCHAR(50) NULL,
|
||||
terminal_public_id VARCHAR(64) NULL,
|
||||
url TEXT NULL,
|
||||
success_redirect_url TEXT NULL,
|
||||
fail_redirect_url TEXT NULL,
|
||||
metadata_json JSON NULL,
|
||||
callback_payload JSON NULL,
|
||||
expires_at DATETIME NULL,
|
||||
transaction_id INTEGER NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (transaction_id) REFERENCES transactions(id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_wata_link_id ON wata_payments(payment_link_id);
|
||||
CREATE INDEX idx_wata_order_id ON wata_payments(order_id);
|
||||
"""
|
||||
|
||||
elif db_type == 'postgresql':
|
||||
create_sql = """
|
||||
CREATE TABLE wata_payments (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
payment_link_id VARCHAR(64) NOT NULL UNIQUE,
|
||||
order_id VARCHAR(255) NULL,
|
||||
amount_kopeks INTEGER NOT NULL,
|
||||
currency VARCHAR(10) NOT NULL DEFAULT 'RUB',
|
||||
description TEXT NULL,
|
||||
type VARCHAR(50) NULL,
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'Opened',
|
||||
is_paid BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
paid_at TIMESTAMP NULL,
|
||||
last_status VARCHAR(50) NULL,
|
||||
terminal_public_id VARCHAR(64) NULL,
|
||||
url TEXT NULL,
|
||||
success_redirect_url TEXT NULL,
|
||||
fail_redirect_url TEXT NULL,
|
||||
metadata_json JSON NULL,
|
||||
callback_payload JSON NULL,
|
||||
expires_at TIMESTAMP NULL,
|
||||
transaction_id INTEGER NULL REFERENCES transactions(id),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_wata_link_id ON wata_payments(payment_link_id);
|
||||
CREATE INDEX idx_wata_order_id ON wata_payments(order_id);
|
||||
"""
|
||||
|
||||
elif db_type == 'mysql':
|
||||
create_sql = """
|
||||
CREATE TABLE wata_payments (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
payment_link_id VARCHAR(64) NOT NULL UNIQUE,
|
||||
order_id VARCHAR(255) NULL,
|
||||
amount_kopeks INT NOT NULL,
|
||||
currency VARCHAR(10) NOT NULL DEFAULT 'RUB',
|
||||
description TEXT NULL,
|
||||
type VARCHAR(50) NULL,
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'Opened',
|
||||
is_paid BOOLEAN NOT NULL DEFAULT 0,
|
||||
paid_at DATETIME NULL,
|
||||
last_status VARCHAR(50) NULL,
|
||||
terminal_public_id VARCHAR(64) NULL,
|
||||
url TEXT NULL,
|
||||
success_redirect_url TEXT NULL,
|
||||
fail_redirect_url TEXT NULL,
|
||||
metadata_json JSON NULL,
|
||||
callback_payload JSON NULL,
|
||||
expires_at DATETIME NULL,
|
||||
transaction_id INT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (transaction_id) REFERENCES transactions(id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_wata_link_id ON wata_payments(payment_link_id);
|
||||
CREATE INDEX idx_wata_order_id ON wata_payments(order_id);
|
||||
"""
|
||||
|
||||
else:
|
||||
logger.error(f"Неподдерживаемый тип БД для таблицы wata_payments: {db_type}")
|
||||
return False
|
||||
|
||||
await conn.execute(text(create_sql))
|
||||
logger.info("Таблица wata_payments успешно создана")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка создания таблицы wata_payments: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def ensure_wata_payment_schema() -> bool:
|
||||
try:
|
||||
table_exists = await check_table_exists("wata_payments")
|
||||
if not table_exists:
|
||||
logger.warning("⚠️ Таблица wata_payments отсутствует — создаём заново")
|
||||
return await create_wata_payments_table()
|
||||
|
||||
link_index_exists = await check_index_exists("wata_payments", "idx_wata_link_id")
|
||||
order_index_exists = await check_index_exists("wata_payments", "idx_wata_order_id")
|
||||
|
||||
async with engine.begin() as conn:
|
||||
db_type = await get_database_type()
|
||||
|
||||
if not link_index_exists:
|
||||
if db_type in {"sqlite", "postgresql"}:
|
||||
await conn.execute(
|
||||
text("CREATE INDEX IF NOT EXISTS idx_wata_link_id ON wata_payments(payment_link_id)")
|
||||
)
|
||||
elif db_type == "mysql":
|
||||
await conn.execute(
|
||||
text("CREATE INDEX idx_wata_link_id ON wata_payments(payment_link_id)")
|
||||
)
|
||||
logger.info("✅ Создан индекс idx_wata_link_id")
|
||||
else:
|
||||
logger.info("ℹ️ Индекс idx_wata_link_id уже существует")
|
||||
|
||||
if not order_index_exists:
|
||||
if db_type in {"sqlite", "postgresql"}:
|
||||
await conn.execute(
|
||||
text("CREATE INDEX IF NOT EXISTS idx_wata_order_id ON wata_payments(order_id)")
|
||||
)
|
||||
elif db_type == "mysql":
|
||||
await conn.execute(
|
||||
text("CREATE INDEX idx_wata_order_id ON wata_payments(order_id)")
|
||||
)
|
||||
logger.info("✅ Создан индекс idx_wata_order_id")
|
||||
else:
|
||||
logger.info("ℹ️ Индекс idx_wata_order_id уже существует")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обновления схемы wata_payments: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def create_discount_offers_table():
|
||||
table_exists = await check_table_exists('discount_offers')
|
||||
if table_exists:
|
||||
@@ -2978,6 +3144,19 @@ async def run_universal_migration():
|
||||
else:
|
||||
logger.warning("⚠️ Проблемы с таблицей Pal24 payments")
|
||||
|
||||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ WATA ===")
|
||||
wata_created = await create_wata_payments_table()
|
||||
if wata_created:
|
||||
logger.info("✅ Таблица Wata payments готова")
|
||||
else:
|
||||
logger.warning("⚠️ Проблемы с таблицей Wata payments")
|
||||
|
||||
wata_schema_ok = await ensure_wata_payment_schema()
|
||||
if wata_schema_ok:
|
||||
logger.info("✅ Схема Wata payments актуальна")
|
||||
else:
|
||||
logger.warning("⚠️ Не удалось обновить схему Wata payments")
|
||||
|
||||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ DISCOUNT_OFFERS ===")
|
||||
discount_created = await create_discount_offers_table()
|
||||
if discount_created:
|
||||
|
||||
@@ -1302,6 +1302,9 @@ def _build_settings_keyboard(
|
||||
elif category_key == "MULENPAY":
|
||||
label = texts.t("PAYMENT_CARD_MULENPAY", "💳 Банковская карта (Mulen Pay)")
|
||||
test_payment_buttons.append([_test_button(f"{label} · тест", "mulenpay")])
|
||||
elif category_key == "WATA":
|
||||
label = texts.t("PAYMENT_CARD_WATA", "💳 Банковская карта (WATA)")
|
||||
test_payment_buttons.append([_test_button(f"{label} · тест", "wata")])
|
||||
elif category_key == "PAL24":
|
||||
label = texts.t("PAYMENT_CARD_PAL24", "💳 Банковская карта (PayPalych)")
|
||||
test_payment_buttons.append([_test_button(f"{label} · тест", "pal24")])
|
||||
|
||||
@@ -390,6 +390,12 @@ async def process_topup_amount(
|
||||
from .mulenpay import process_mulenpay_payment_amount
|
||||
async with AsyncSessionLocal() as db:
|
||||
await process_mulenpay_payment_amount(message, db_user, db, amount_kopeks, state)
|
||||
elif payment_method == "wata":
|
||||
from app.database.database import AsyncSessionLocal
|
||||
from .wata import process_wata_payment_amount
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
await process_wata_payment_amount(message, db_user, db, amount_kopeks, state)
|
||||
elif payment_method == "pal24":
|
||||
from app.database.database import AsyncSessionLocal
|
||||
from .pal24 import process_pal24_payment_amount
|
||||
@@ -490,6 +496,14 @@ async def handle_quick_amount_selection(
|
||||
await process_mulenpay_payment_amount(
|
||||
callback.message, db_user, db, amount_kopeks, state
|
||||
)
|
||||
elif payment_method == "wata":
|
||||
from app.database.database import AsyncSessionLocal
|
||||
from .wata import process_wata_payment_amount
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
await process_wata_payment_amount(
|
||||
callback.message, db_user, db, amount_kopeks, state
|
||||
)
|
||||
elif payment_method == "pal24":
|
||||
from app.database.database import AsyncSessionLocal
|
||||
from .pal24 import process_pal24_payment_amount
|
||||
@@ -627,6 +641,12 @@ def register_balance_handlers(dp: Dispatcher):
|
||||
F.data == "topup_mulenpay"
|
||||
)
|
||||
|
||||
from .wata import start_wata_payment
|
||||
dp.callback_query.register(
|
||||
start_wata_payment,
|
||||
F.data == "topup_wata"
|
||||
)
|
||||
|
||||
from .pal24 import start_pal24_payment
|
||||
dp.callback_query.register(
|
||||
start_pal24_payment,
|
||||
@@ -679,6 +699,12 @@ def register_balance_handlers(dp: Dispatcher):
|
||||
F.data.startswith("check_mulenpay_")
|
||||
)
|
||||
|
||||
from .wata import check_wata_payment_status
|
||||
dp.callback_query.register(
|
||||
check_wata_payment_status,
|
||||
F.data.startswith("check_wata_")
|
||||
)
|
||||
|
||||
from .pal24 import check_pal24_payment_status
|
||||
dp.callback_query.register(
|
||||
check_pal24_payment_status,
|
||||
|
||||
234
app/handlers/balance/wata.py
Normal file
234
app/handlers/balance/wata.py
Normal file
@@ -0,0 +1,234 @@
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
from aiogram import types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import settings
|
||||
from app.database.models import User
|
||||
from app.keyboards.inline import get_back_keyboard
|
||||
from app.localization.texts import get_texts
|
||||
from app.services.payment_service import PaymentService, get_user_by_id as fetch_user_by_id
|
||||
from app.states import BalanceStates
|
||||
from app.utils.decorators import error_handler
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@error_handler
|
||||
async def start_wata_payment(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
state: FSMContext,
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
if not settings.is_wata_enabled():
|
||||
await callback.answer("❌ Оплата через WATA временно недоступна", show_alert=True)
|
||||
return
|
||||
|
||||
message_text = texts.t(
|
||||
"WATA_TOPUP_PROMPT",
|
||||
(
|
||||
"💳 <b>Оплата через WATA</b>\n\n"
|
||||
"Введите сумму пополнения. Минимальная сумма — {min_amount}, максимальная — {max_amount}.\n"
|
||||
"Оплата происходит через защищенную форму WATA."
|
||||
),
|
||||
).format(
|
||||
min_amount=settings.format_price(settings.WATA_MIN_AMOUNT_KOPEKS),
|
||||
max_amount=settings.format_price(settings.WATA_MAX_AMOUNT_KOPEKS),
|
||||
)
|
||||
|
||||
keyboard = get_back_keyboard(db_user.language)
|
||||
|
||||
if settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not settings.DISABLE_TOPUP_BUTTONS:
|
||||
from .main import get_quick_amount_buttons
|
||||
|
||||
quick_amount_buttons = get_quick_amount_buttons(db_user.language)
|
||||
if quick_amount_buttons:
|
||||
keyboard.inline_keyboard = quick_amount_buttons + keyboard.inline_keyboard
|
||||
|
||||
await callback.message.edit_text(
|
||||
message_text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
await state.set_state(BalanceStates.waiting_for_amount)
|
||||
await state.update_data(payment_method="wata")
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@error_handler
|
||||
async def process_wata_payment_amount(
|
||||
message: types.Message,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
amount_kopeks: int,
|
||||
state: FSMContext,
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
if not settings.is_wata_enabled():
|
||||
await message.answer("❌ Оплата через WATA временно недоступна")
|
||||
return
|
||||
|
||||
if amount_kopeks < settings.WATA_MIN_AMOUNT_KOPEKS:
|
||||
await message.answer(
|
||||
texts.t(
|
||||
"WATA_AMOUNT_TOO_LOW",
|
||||
"Минимальная сумма пополнения: {amount}",
|
||||
).format(amount=settings.format_price(settings.WATA_MIN_AMOUNT_KOPEKS))
|
||||
)
|
||||
return
|
||||
|
||||
if amount_kopeks > settings.WATA_MAX_AMOUNT_KOPEKS:
|
||||
await message.answer(
|
||||
texts.t(
|
||||
"WATA_AMOUNT_TOO_HIGH",
|
||||
"Максимальная сумма пополнения: {amount}",
|
||||
).format(amount=settings.format_price(settings.WATA_MAX_AMOUNT_KOPEKS))
|
||||
)
|
||||
return
|
||||
|
||||
payment_service = PaymentService(message.bot)
|
||||
|
||||
try:
|
||||
result = await payment_service.create_wata_payment(
|
||||
db=db,
|
||||
user_id=db_user.id,
|
||||
amount_kopeks=amount_kopeks,
|
||||
description=settings.get_balance_payment_description(amount_kopeks),
|
||||
language=db_user.language,
|
||||
)
|
||||
except Exception as error: # pragma: no cover - handled by decorator logs
|
||||
logger.exception("Ошибка создания WATA платежа: %s", error)
|
||||
result = None
|
||||
|
||||
if not result or not result.get("payment_url"):
|
||||
await message.answer(
|
||||
texts.t(
|
||||
"WATA_PAYMENT_ERROR",
|
||||
"❌ Ошибка создания платежа WATA. Попробуйте позже или обратитесь в поддержку.",
|
||||
)
|
||||
)
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
payment_url = result["payment_url"]
|
||||
payment_link_id = result["payment_link_id"]
|
||||
local_payment_id = result["local_payment_id"]
|
||||
|
||||
keyboard = types.InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text=texts.t("WATA_PAY_BUTTON", "💳 Оплатить через WATA"),
|
||||
url=payment_url,
|
||||
)
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text=texts.t("CHECK_STATUS_BUTTON", "📊 Проверить статус"),
|
||||
callback_data=f"check_wata_{local_payment_id}",
|
||||
)
|
||||
],
|
||||
[types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")],
|
||||
]
|
||||
)
|
||||
|
||||
message_template = texts.t(
|
||||
"WATA_PAYMENT_INSTRUCTIONS",
|
||||
(
|
||||
"💳 <b>Оплата через WATA</b>\n\n"
|
||||
"💰 Сумма: {amount}\n"
|
||||
"🆔 ID платежа: {payment_id}\n\n"
|
||||
"📱 <b>Инструкция:</b>\n"
|
||||
"1. Нажмите кнопку 'Оплатить через WATA'\n"
|
||||
"2. Следуйте подсказкам платежной системы\n"
|
||||
"3. Подтвердите перевод\n"
|
||||
"4. Средства зачислятся автоматически\n\n"
|
||||
"❓ Если возникнут проблемы, обратитесь в {support}"
|
||||
),
|
||||
)
|
||||
|
||||
message_text = message_template.format(
|
||||
amount=settings.format_price(amount_kopeks),
|
||||
payment_id=payment_link_id,
|
||||
support=settings.get_support_contact_display_html(),
|
||||
)
|
||||
|
||||
await message.answer(
|
||||
message_text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
await state.clear()
|
||||
|
||||
logger.info(
|
||||
"Создан WATA платеж для пользователя %s: %s₽, ссылка: %s",
|
||||
db_user.telegram_id,
|
||||
amount_kopeks / 100,
|
||||
payment_link_id,
|
||||
)
|
||||
|
||||
|
||||
@error_handler
|
||||
async def check_wata_payment_status(
|
||||
callback: types.CallbackQuery,
|
||||
db: AsyncSession,
|
||||
):
|
||||
try:
|
||||
local_payment_id = int(callback.data.split("_")[-1])
|
||||
except (ValueError, IndexError):
|
||||
await callback.answer("❌ Некорректный идентификатор платежа", show_alert=True)
|
||||
return
|
||||
|
||||
payment_service = PaymentService(callback.bot)
|
||||
status_info = await payment_service.get_wata_payment_status(db, local_payment_id)
|
||||
|
||||
if not status_info:
|
||||
await callback.answer("❌ Платеж не найден", show_alert=True)
|
||||
return
|
||||
|
||||
payment = status_info["payment"]
|
||||
|
||||
user_language = "ru"
|
||||
try:
|
||||
user = await fetch_user_by_id(db, payment.user_id)
|
||||
if user and getattr(user, "language", None):
|
||||
user_language = user.language
|
||||
except Exception as error:
|
||||
logger.debug("Не удалось получить пользователя для WATA статуса: %s", error)
|
||||
|
||||
texts = get_texts(user_language)
|
||||
|
||||
status_labels: Dict[str, Dict[str, str]] = {
|
||||
"Opened": {"emoji": "⏳", "label": texts.t("WATA_STATUS_OPENED", "Ожидает оплаты")},
|
||||
"Closed": {"emoji": "⌛", "label": texts.t("WATA_STATUS_CLOSED", "Обрабатывается")},
|
||||
"Paid": {"emoji": "✅", "label": texts.t("WATA_STATUS_PAID", "Оплачен")},
|
||||
"Declined": {"emoji": "❌", "label": texts.t("WATA_STATUS_DECLINED", "Отклонен")},
|
||||
}
|
||||
|
||||
label_info = status_labels.get(payment.status, {"emoji": "❓", "label": texts.t("WATA_STATUS_UNKNOWN", "Неизвестно")})
|
||||
|
||||
message_lines = [
|
||||
texts.t("WATA_STATUS_TITLE", "💳 <b>Статус платежа WATA</b>"),
|
||||
"",
|
||||
f"🆔 ID: {payment.payment_link_id}",
|
||||
f"💰 Сумма: {settings.format_price(payment.amount_kopeks)}",
|
||||
f"📊 Статус: {label_info['emoji']} {label_info['label']}",
|
||||
f"📅 Создан: {payment.created_at.strftime('%d.%m.%Y %H:%M') if payment.created_at else '—'}",
|
||||
]
|
||||
|
||||
if payment.is_paid:
|
||||
message_lines.append("\n✅ Платеж успешно завершен! Средства уже на балансе.")
|
||||
elif payment.status in {"Opened", "Closed"}:
|
||||
message_lines.append(
|
||||
"\n⏳ Платеж еще не завершен. Завершите оплату по ссылке и проверьте статус позже."
|
||||
)
|
||||
|
||||
await callback.message.answer("\n".join(message_lines), parse_mode="HTML")
|
||||
await callback.answer()
|
||||
@@ -1096,6 +1096,14 @@ def get_payment_methods_keyboard(amount_kopeks: int, language: str = DEFAULT_LAN
|
||||
)
|
||||
])
|
||||
|
||||
if settings.is_wata_enabled():
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text=texts.t("PAYMENT_CARD_WATA", "💳 Банковская карта (WATA)"),
|
||||
callback_data=_build_callback("wata")
|
||||
)
|
||||
])
|
||||
|
||||
if settings.is_pal24_enabled():
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
|
||||
@@ -621,11 +621,26 @@
|
||||
"PAL24_PAY_BUTTON": "🏦 Pay with PayPalych (SBP)",
|
||||
"PAL24_TOPUP_PROMPT": "🏦 <b>PayPalych (SBP) payment</b>\n\nEnter an amount between 100 and 1,000,000 ₽.\nThe payment is processed via the PayPalych Faster Payments System.",
|
||||
"PAYMENT_CARD_MULENPAY": "💳 Bank card (Mulen Pay)",
|
||||
"PAYMENT_CARD_WATA": "💳 Bank card (WATA)",
|
||||
"PAYMENT_CARD_PAL24": "🏦 SBP (PayPalych)",
|
||||
"PAYMENT_METHOD_MULENPAY_DESCRIPTION": "via Mulen Pay",
|
||||
"PAYMENT_METHOD_MULENPAY_NAME": "💳 <b>Bank card (Mulen Pay)</b>",
|
||||
"PAYMENT_METHOD_WATA_DESCRIPTION": "via WATA",
|
||||
"PAYMENT_METHOD_WATA_NAME": "💳 <b>Bank card (WATA)</b>",
|
||||
"PAYMENT_METHOD_PAL24_DESCRIPTION": "via Faster Payments System",
|
||||
"PAYMENT_METHOD_PAL24_NAME": "🏦 <b>SBP (PayPalych)</b>",
|
||||
"WATA_TOPUP_PROMPT": "💳 <b>Pay via WATA</b>\n\nEnter the amount to top up. Minimum amount — {min_amount}, maximum — {max_amount}.\nPayments are processed through the secure WATA form.",
|
||||
"WATA_AMOUNT_TOO_LOW": "Minimum top-up amount: {amount}",
|
||||
"WATA_AMOUNT_TOO_HIGH": "Maximum top-up amount: {amount}",
|
||||
"WATA_PAYMENT_ERROR": "❌ Failed to create WATA payment. Please try again later or contact support.",
|
||||
"WATA_PAY_BUTTON": "💳 Pay via WATA",
|
||||
"WATA_PAYMENT_INSTRUCTIONS": "💳 <b>Pay via WATA</b>\n\n💰 Amount: {amount}\n🆔 Payment ID: {payment_id}\n\n📱 <b>How to pay:</b>\n1. Tap ‘Pay via WATA’\n2. Follow the gateway prompts\n3. Confirm the transfer\n4. Funds will be credited automatically\n\n❓ Need help? Contact {support}",
|
||||
"WATA_STATUS_TITLE": "💳 <b>WATA payment status</b>",
|
||||
"WATA_STATUS_OPENED": "Waiting for payment",
|
||||
"WATA_STATUS_CLOSED": "Processing",
|
||||
"WATA_STATUS_PAID": "Paid",
|
||||
"WATA_STATUS_DECLINED": "Declined",
|
||||
"WATA_STATUS_UNKNOWN": "Unknown",
|
||||
"REPLY_TO_TICKET": "💬 Reply",
|
||||
"REPORT_CLOSE": "❌ Close",
|
||||
"REPORT_CLOSED": "✅ Report closed.",
|
||||
|
||||
@@ -629,11 +629,26 @@
|
||||
"PAL24_PAY_BUTTON": "🏦 Оплатить через PayPalych (СБП)",
|
||||
"PAL24_TOPUP_PROMPT": "🏦 <b>Оплата через PayPalych (СБП)</b>\n\nВведите сумму для пополнения от 100 до 1 000 000 ₽.\nОплата проходит через систему быстрых платежей PayPalych.",
|
||||
"PAYMENT_CARD_MULENPAY": "💳 Банковская карта (Mulen Pay)",
|
||||
"PAYMENT_CARD_WATA": "💳 Банковская карта (WATA)",
|
||||
"PAYMENT_CARD_PAL24": "🏦 СБП (PayPalych)",
|
||||
"PAYMENT_METHOD_MULENPAY_DESCRIPTION": "через Mulen Pay",
|
||||
"PAYMENT_METHOD_MULENPAY_NAME": "💳 <b>Банковская карта (Mulen Pay)</b>",
|
||||
"PAYMENT_METHOD_WATA_DESCRIPTION": "через WATA",
|
||||
"PAYMENT_METHOD_WATA_NAME": "💳 <b>Банковская карта (WATA)</b>",
|
||||
"PAYMENT_METHOD_PAL24_DESCRIPTION": "через систему быстрых платежей",
|
||||
"PAYMENT_METHOD_PAL24_NAME": "🏦 <b>СБП (PayPalych)</b>",
|
||||
"WATA_TOPUP_PROMPT": "💳 <b>Оплата через WATA</b>\n\nВведите сумму пополнения. Минимальная сумма — {min_amount}, максимальная — {max_amount}.\nОплата происходит через защищенную форму WATA.",
|
||||
"WATA_AMOUNT_TOO_LOW": "Минимальная сумма пополнения: {amount}",
|
||||
"WATA_AMOUNT_TOO_HIGH": "Максимальная сумма пополнения: {amount}",
|
||||
"WATA_PAYMENT_ERROR": "❌ Ошибка создания платежа WATA. Попробуйте позже или обратитесь в поддержку.",
|
||||
"WATA_PAY_BUTTON": "💳 Оплатить через WATA",
|
||||
"WATA_PAYMENT_INSTRUCTIONS": "💳 <b>Оплата через WATA</b>\n\n💰 Сумма: {amount}\n🆔 ID платежа: {payment_id}\n\n📱 <b>Инструкция:</b>\n1. Нажмите кнопку 'Оплатить через WATA'\n2. Следуйте подсказкам платежной системы\n3. Подтвердите перевод\n4. Средства зачислятся автоматически\n\n❓ Если возникнут проблемы, обратитесь в {support}",
|
||||
"WATA_STATUS_TITLE": "💳 <b>Статус платежа WATA</b>",
|
||||
"WATA_STATUS_OPENED": "Ожидает оплаты",
|
||||
"WATA_STATUS_CLOSED": "Обрабатывается",
|
||||
"WATA_STATUS_PAID": "Оплачен",
|
||||
"WATA_STATUS_DECLINED": "Отклонен",
|
||||
"WATA_STATUS_UNKNOWN": "Неизвестно",
|
||||
"REPLY_TO_TICKET": "💬 Ответить",
|
||||
"REPORT_CLOSE": "❌ Закрыть",
|
||||
"REPORT_CLOSED": "✅ Отчет закрыт.",
|
||||
|
||||
@@ -11,6 +11,7 @@ from .tribute import TributePaymentMixin
|
||||
from .cryptobot import CryptoBotPaymentMixin
|
||||
from .mulenpay import MulenPayPaymentMixin
|
||||
from .pal24 import Pal24PaymentMixin
|
||||
from .wata import WataPaymentMixin
|
||||
|
||||
__all__ = [
|
||||
"PaymentCommonMixin",
|
||||
@@ -20,4 +21,5 @@ __all__ = [
|
||||
"CryptoBotPaymentMixin",
|
||||
"MulenPayPaymentMixin",
|
||||
"Pal24PaymentMixin",
|
||||
"WataPaymentMixin",
|
||||
]
|
||||
|
||||
357
app/services/payment/wata.py
Normal file
357
app/services/payment/wata.py
Normal file
@@ -0,0 +1,357 @@
|
||||
"""Mixin for integrating WATA payment links into the payment service."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from importlib import import_module
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import settings
|
||||
from app.database.models import PaymentMethod, TransactionType
|
||||
from app.services.wata_service import WataAPIError, WataService
|
||||
from app.utils.user_utils import format_referrer_info
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WataPaymentMixin:
|
||||
"""Encapsulates creation and status handling for WATA payment links."""
|
||||
|
||||
async def create_wata_payment(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
amount_kopeks: int,
|
||||
description: str,
|
||||
*,
|
||||
language: Optional[str] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
if not getattr(self, "wata_service", None):
|
||||
logger.error("WATA service is not initialised")
|
||||
return None
|
||||
|
||||
if amount_kopeks < settings.WATA_MIN_AMOUNT_KOPEKS:
|
||||
logger.warning(
|
||||
"Сумма WATA меньше минимальной: %s < %s",
|
||||
amount_kopeks,
|
||||
settings.WATA_MIN_AMOUNT_KOPEKS,
|
||||
)
|
||||
return None
|
||||
|
||||
if amount_kopeks > settings.WATA_MAX_AMOUNT_KOPEKS:
|
||||
logger.warning(
|
||||
"Сумма WATA больше максимальной: %s > %s",
|
||||
amount_kopeks,
|
||||
settings.WATA_MAX_AMOUNT_KOPEKS,
|
||||
)
|
||||
return None
|
||||
|
||||
payment_module = import_module("app.services.payment_service")
|
||||
|
||||
order_id = f"wata_{user_id}_{uuid.uuid4().hex[:12]}"
|
||||
|
||||
try:
|
||||
response = await self.wata_service.create_payment_link( # type: ignore[union-attr]
|
||||
amount_kopeks=amount_kopeks,
|
||||
currency="RUB",
|
||||
description=description,
|
||||
order_id=order_id,
|
||||
)
|
||||
except WataAPIError as error:
|
||||
logger.error("Ошибка создания WATA платежа: %s", error)
|
||||
return None
|
||||
except Exception as error: # pragma: no cover - safety net
|
||||
logger.exception("Непредвиденная ошибка при создании WATA платежа: %s", error)
|
||||
return None
|
||||
|
||||
payment_link_id = response.get("id")
|
||||
payment_url = response.get("url") or response.get("paymentUrl")
|
||||
status = response.get("status") or "Opened"
|
||||
terminal_public_id = response.get("terminalPublicId")
|
||||
success_url = response.get("successRedirectUrl")
|
||||
fail_url = response.get("failRedirectUrl")
|
||||
|
||||
if not payment_link_id:
|
||||
logger.error("WATA API не вернула идентификатор платежной ссылки: %s", response)
|
||||
return None
|
||||
|
||||
expiration_raw = response.get("expirationDateTime")
|
||||
expires_at = WataService._parse_datetime(expiration_raw)
|
||||
|
||||
metadata = {
|
||||
"response": response,
|
||||
"language": language or settings.DEFAULT_LANGUAGE,
|
||||
}
|
||||
|
||||
local_payment = await payment_module.create_wata_payment(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
payment_link_id=payment_link_id,
|
||||
amount_kopeks=amount_kopeks,
|
||||
currency="RUB",
|
||||
description=description,
|
||||
status=status,
|
||||
type_=response.get("type"),
|
||||
url=payment_url,
|
||||
order_id=order_id,
|
||||
metadata=metadata,
|
||||
expires_at=expires_at,
|
||||
terminal_public_id=terminal_public_id,
|
||||
success_redirect_url=success_url,
|
||||
fail_redirect_url=fail_url,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Создан WATA платеж %s на %s₽ для пользователя %s",
|
||||
payment_link_id,
|
||||
amount_kopeks / 100,
|
||||
user_id,
|
||||
)
|
||||
|
||||
return {
|
||||
"local_payment_id": local_payment.id,
|
||||
"payment_link_id": payment_link_id,
|
||||
"payment_url": payment_url,
|
||||
"status": status,
|
||||
"order_id": order_id,
|
||||
}
|
||||
|
||||
async def get_wata_payment_status(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
local_payment_id: int,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
payment_module = import_module("app.services.payment_service")
|
||||
|
||||
payment = await payment_module.get_wata_payment_by_id(db, local_payment_id)
|
||||
if not payment:
|
||||
return None
|
||||
|
||||
remote_link: Optional[Dict[str, Any]] = None
|
||||
transaction_payload: Optional[Dict[str, Any]] = None
|
||||
|
||||
if getattr(self, "wata_service", None) and payment.payment_link_id:
|
||||
try:
|
||||
remote_link = await self.wata_service.get_payment_link(payment.payment_link_id) # type: ignore[union-attr]
|
||||
except WataAPIError as error:
|
||||
logger.error("Ошибка получения WATA ссылки %s: %s", payment.payment_link_id, error)
|
||||
except Exception as error: # pragma: no cover - safety net
|
||||
logger.exception("Непредвиденная ошибка при запросе WATA ссылки: %s", error)
|
||||
|
||||
if remote_link:
|
||||
remote_status = remote_link.get("status") or payment.status
|
||||
if remote_status != payment.status:
|
||||
existing_metadata = dict(getattr(payment, "metadata_json", {}) or {})
|
||||
existing_metadata["link"] = remote_link
|
||||
await payment_module.update_wata_payment_status(
|
||||
db,
|
||||
payment=payment,
|
||||
status=remote_status,
|
||||
last_status=remote_status,
|
||||
url=remote_link.get("url") or remote_link.get("paymentUrl"),
|
||||
metadata=existing_metadata,
|
||||
terminal_public_id=remote_link.get("terminalPublicId"),
|
||||
)
|
||||
payment = await payment_module.get_wata_payment_by_id(db, local_payment_id)
|
||||
|
||||
if (remote_status or "").lower() == "closed" and not payment.is_paid:
|
||||
try:
|
||||
tx_response = await self.wata_service.search_transactions( # type: ignore[union-attr]
|
||||
order_id=payment.order_id,
|
||||
payment_link_id=payment.payment_link_id,
|
||||
status="Paid",
|
||||
limit=5,
|
||||
)
|
||||
items = tx_response.get("items") or []
|
||||
for item in items:
|
||||
if (item or {}).get("status") == "Paid":
|
||||
transaction_payload = item
|
||||
break
|
||||
except WataAPIError as error:
|
||||
logger.error(
|
||||
"Ошибка поиска WATA транзакций для %s: %s",
|
||||
payment.payment_link_id,
|
||||
error,
|
||||
)
|
||||
except Exception as error: # pragma: no cover - safety net
|
||||
logger.exception("Непредвиденная ошибка при поиске WATA транзакции: %s", error)
|
||||
|
||||
if transaction_payload and not payment.is_paid:
|
||||
payment = await self._finalize_wata_payment(db, payment, transaction_payload)
|
||||
|
||||
return {
|
||||
"payment": payment,
|
||||
"status": payment.status,
|
||||
"is_paid": payment.is_paid,
|
||||
"remote_link": remote_link,
|
||||
"transaction": transaction_payload,
|
||||
}
|
||||
|
||||
async def _finalize_wata_payment(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
payment: Any,
|
||||
transaction_payload: Dict[str, Any],
|
||||
) -> Any:
|
||||
payment_module = import_module("app.services.payment_service")
|
||||
|
||||
paid_at = WataService._parse_datetime(transaction_payload.get("paymentTime"))
|
||||
existing_metadata = dict(getattr(payment, "metadata_json", {}) or {})
|
||||
existing_metadata["transaction"] = transaction_payload
|
||||
|
||||
await payment_module.update_wata_payment_status(
|
||||
db,
|
||||
payment=payment,
|
||||
status="Paid",
|
||||
is_paid=True,
|
||||
paid_at=paid_at,
|
||||
callback_payload=transaction_payload,
|
||||
metadata=existing_metadata,
|
||||
)
|
||||
|
||||
if payment.transaction_id:
|
||||
logger.info(
|
||||
"WATA платеж %s уже привязан к транзакции %s",
|
||||
payment.payment_link_id,
|
||||
payment.transaction_id,
|
||||
)
|
||||
return payment
|
||||
|
||||
user = await payment_module.get_user_by_id(db, payment.user_id)
|
||||
if not user:
|
||||
logger.error("Пользователь %s не найден при обработке WATA", payment.user_id)
|
||||
return payment
|
||||
|
||||
transaction_external_id = str(transaction_payload.get("id") or transaction_payload.get("transactionId") or "")
|
||||
description = f"Пополнение через WATA ({payment.payment_link_id})"
|
||||
|
||||
transaction = await payment_module.create_transaction(
|
||||
db,
|
||||
user_id=payment.user_id,
|
||||
type=TransactionType.DEPOSIT,
|
||||
amount_kopeks=payment.amount_kopeks,
|
||||
description=description,
|
||||
payment_method=PaymentMethod.WATA,
|
||||
external_id=transaction_external_id or payment.payment_link_id,
|
||||
is_completed=True,
|
||||
)
|
||||
|
||||
await payment_module.link_wata_payment_to_transaction(db, payment, transaction.id)
|
||||
|
||||
old_balance = user.balance_kopeks
|
||||
was_first_topup = not user.has_made_first_topup
|
||||
|
||||
user.balance_kopeks += payment.amount_kopeks
|
||||
user.updated_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
|
||||
promo_group = getattr(user, "promo_group", None)
|
||||
subscription = getattr(user, "subscription", None)
|
||||
referrer_info = format_referrer_info(user)
|
||||
topup_status = "🆕 Первое пополнение" if was_first_topup else "🔄 Пополнение"
|
||||
|
||||
try:
|
||||
from app.services.referral_service import process_referral_topup
|
||||
|
||||
await process_referral_topup(
|
||||
db,
|
||||
user.id,
|
||||
payment.amount_kopeks,
|
||||
getattr(self, "bot", None),
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error("Ошибка обработки реферального пополнения WATA: %s", error)
|
||||
|
||||
if was_first_topup and not user.has_made_first_topup:
|
||||
user.has_made_first_topup = True
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
|
||||
if getattr(self, "bot", None):
|
||||
try:
|
||||
from app.services.admin_notification_service import AdminNotificationService
|
||||
|
||||
notification_service = AdminNotificationService(self.bot)
|
||||
await notification_service.send_balance_topup_notification(
|
||||
user,
|
||||
transaction,
|
||||
old_balance,
|
||||
topup_status=topup_status,
|
||||
referrer_info=referrer_info,
|
||||
subscription=subscription,
|
||||
promo_group=promo_group,
|
||||
db=db,
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error("Ошибка отправки админ уведомления WATA: %s", error)
|
||||
|
||||
if getattr(self, "bot", None):
|
||||
try:
|
||||
keyboard = await self.build_topup_success_keyboard(user)
|
||||
await self.bot.send_message(
|
||||
user.telegram_id,
|
||||
(
|
||||
"✅ <b>Пополнение успешно!</b>\n\n"
|
||||
f"💰 Сумма: {settings.format_price(payment.amount_kopeks)}\n"
|
||||
"🦊 Способ: WATA\n"
|
||||
f"🆔 Транзакция: {transaction.id}\n\n"
|
||||
"Баланс пополнен автоматически!"
|
||||
),
|
||||
parse_mode="HTML",
|
||||
reply_markup=keyboard,
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error("Ошибка отправки уведомления пользователю WATA: %s", error)
|
||||
|
||||
try:
|
||||
from app.services.user_cart_service import user_cart_service
|
||||
from aiogram import types
|
||||
|
||||
has_saved_cart = await user_cart_service.has_user_cart(user.id)
|
||||
if has_saved_cart and getattr(self, "bot", None):
|
||||
from app.localization.texts import get_texts
|
||||
|
||||
texts = get_texts(user.language)
|
||||
cart_message = texts.t(
|
||||
"BALANCE_TOPUP_CART_REMINDER_DETAILED",
|
||||
"🛒 У вас есть неоформленный заказ.\n\n"
|
||||
"Вы можете продолжить оформление с теми же параметрами.",
|
||||
)
|
||||
|
||||
keyboard = types.InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text=texts.RETURN_TO_SUBSCRIPTION_CHECKOUT,
|
||||
callback_data="subscription_resume_checkout",
|
||||
)
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text="💰 Мой баланс",
|
||||
callback_data="menu_balance",
|
||||
)
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text="🏠 Главное меню",
|
||||
callback_data="back_to_menu",
|
||||
)
|
||||
],
|
||||
]
|
||||
)
|
||||
|
||||
await self.bot.send_message(
|
||||
user.telegram_id,
|
||||
cart_message,
|
||||
reply_markup=keyboard,
|
||||
)
|
||||
except Exception as error:
|
||||
logger.debug("Не удалось отправить напоминание о корзине после WATA: %s", error)
|
||||
|
||||
return payment
|
||||
@@ -22,8 +22,10 @@ from app.services.payment import (
|
||||
TelegramStarsMixin,
|
||||
TributePaymentMixin,
|
||||
YooKassaPaymentMixin,
|
||||
WataPaymentMixin,
|
||||
)
|
||||
from app.services.yookassa_service import YooKassaService
|
||||
from app.services.wata_service import WataService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -126,6 +128,36 @@ async def link_pal24_payment_to_transaction(*args, **kwargs):
|
||||
return await pal_crud.link_pal24_payment_to_transaction(*args, **kwargs)
|
||||
|
||||
|
||||
async def create_wata_payment(*args, **kwargs):
|
||||
wata_crud = import_module("app.database.crud.wata")
|
||||
return await wata_crud.create_wata_payment(*args, **kwargs)
|
||||
|
||||
|
||||
async def get_wata_payment_by_link_id(*args, **kwargs):
|
||||
wata_crud = import_module("app.database.crud.wata")
|
||||
return await wata_crud.get_wata_payment_by_link_id(*args, **kwargs)
|
||||
|
||||
|
||||
async def get_wata_payment_by_id(*args, **kwargs):
|
||||
wata_crud = import_module("app.database.crud.wata")
|
||||
return await wata_crud.get_wata_payment_by_id(*args, **kwargs)
|
||||
|
||||
|
||||
async def get_wata_payment_by_order_id(*args, **kwargs):
|
||||
wata_crud = import_module("app.database.crud.wata")
|
||||
return await wata_crud.get_wata_payment_by_order_id(*args, **kwargs)
|
||||
|
||||
|
||||
async def update_wata_payment_status(*args, **kwargs):
|
||||
wata_crud = import_module("app.database.crud.wata")
|
||||
return await wata_crud.update_wata_payment_status(*args, **kwargs)
|
||||
|
||||
|
||||
async def link_wata_payment_to_transaction(*args, **kwargs):
|
||||
wata_crud = import_module("app.database.crud.wata")
|
||||
return await wata_crud.link_wata_payment_to_transaction(*args, **kwargs)
|
||||
|
||||
|
||||
async def create_cryptobot_payment(*args, **kwargs):
|
||||
crypto_crud = import_module("app.database.crud.cryptobot")
|
||||
return await crypto_crud.create_cryptobot_payment(*args, **kwargs)
|
||||
@@ -154,6 +186,7 @@ class PaymentService(
|
||||
CryptoBotPaymentMixin,
|
||||
MulenPayPaymentMixin,
|
||||
Pal24PaymentMixin,
|
||||
WataPaymentMixin,
|
||||
):
|
||||
"""Основной интерфейс платежей, делегирующий работу специализированным mixin-ам."""
|
||||
|
||||
@@ -174,13 +207,15 @@ class PaymentService(
|
||||
self.pal24_service = (
|
||||
Pal24Service() if settings.is_pal24_enabled() else None
|
||||
)
|
||||
self.wata_service = WataService() if settings.is_wata_enabled() else None
|
||||
|
||||
logger.debug(
|
||||
"PaymentService инициализирован (YooKassa=%s, Stars=%s, CryptoBot=%s, "
|
||||
"MulenPay=%s, Pal24=%s)",
|
||||
"MulenPay=%s, Pal24=%s, Wata=%s)",
|
||||
bool(self.yookassa_service),
|
||||
bool(self.stars_service),
|
||||
bool(self.cryptobot_service),
|
||||
bool(self.mulenpay_service),
|
||||
bool(self.pal24_service),
|
||||
bool(self.wata_service),
|
||||
)
|
||||
|
||||
201
app/services/wata_service.py
Normal file
201
app/services/wata_service.py
Normal file
@@ -0,0 +1,201 @@
|
||||
"""High level service for interacting with WATA payment API."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import aiohttp
|
||||
|
||||
from app.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WataAPIError(RuntimeError):
|
||||
"""Raised when the WATA API returns an error response."""
|
||||
|
||||
|
||||
class WataService:
|
||||
"""Thin wrapper around the WATA REST API used for balance top-ups."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
base_url: Optional[str] = None,
|
||||
access_token: Optional[str] = None,
|
||||
request_timeout: Optional[int] = None,
|
||||
) -> None:
|
||||
self.base_url = (base_url or settings.WATA_BASE_URL or "").rstrip("/")
|
||||
self.access_token = access_token or settings.WATA_ACCESS_TOKEN
|
||||
self.request_timeout = request_timeout or int(settings.WATA_REQUEST_TIMEOUT)
|
||||
|
||||
@property
|
||||
def is_configured(self) -> bool:
|
||||
return bool(
|
||||
settings.is_wata_enabled()
|
||||
and self.base_url
|
||||
and self.access_token
|
||||
)
|
||||
|
||||
def _build_url(self, path: str) -> str:
|
||||
return f"{self.base_url}/{path.lstrip('/')}"
|
||||
|
||||
def _build_headers(self) -> Dict[str, str]:
|
||||
if not self.access_token:
|
||||
raise WataAPIError("WATA access token is not configured")
|
||||
return {
|
||||
"Authorization": f"Bearer {self.access_token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
async def _request(
|
||||
self,
|
||||
method: str,
|
||||
path: str,
|
||||
*,
|
||||
json: Optional[Dict[str, Any]] = None,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
if not self.is_configured:
|
||||
raise WataAPIError("WATA service is not configured")
|
||||
|
||||
url = self._build_url(path)
|
||||
timeout = aiohttp.ClientTimeout(total=self.request_timeout)
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
async with session.request(
|
||||
method,
|
||||
url,
|
||||
json=json,
|
||||
params=params,
|
||||
headers=self._build_headers(),
|
||||
) as response:
|
||||
response_text = await response.text()
|
||||
if response.status >= 400:
|
||||
logger.error(
|
||||
"WATA API error %s: %s", response.status, response_text
|
||||
)
|
||||
raise WataAPIError(
|
||||
f"WATA API returned status {response.status}: {response_text}"
|
||||
)
|
||||
|
||||
if not response_text:
|
||||
return {}
|
||||
|
||||
try:
|
||||
data = await response.json()
|
||||
except aiohttp.ContentTypeError as error:
|
||||
logger.error("WATA API returned non-JSON response: %s", error)
|
||||
raise WataAPIError("WATA API returned invalid JSON") from error
|
||||
|
||||
return data
|
||||
except aiohttp.ClientError as error:
|
||||
logger.error("Error communicating with WATA API: %s", error)
|
||||
raise WataAPIError("Failed to communicate with WATA API") from error
|
||||
|
||||
@staticmethod
|
||||
def _amount_from_kopeks(amount_kopeks: int) -> float:
|
||||
return round(amount_kopeks / 100, 2)
|
||||
|
||||
@staticmethod
|
||||
def _format_datetime(value: datetime) -> str:
|
||||
aware = value.astimezone(timezone.utc)
|
||||
return aware.replace(tzinfo=timezone.utc).isoformat().replace("+00:00", "Z")
|
||||
|
||||
@staticmethod
|
||||
def _parse_datetime(raw: Optional[str]) -> Optional[datetime]:
|
||||
if not raw:
|
||||
return None
|
||||
try:
|
||||
normalized = raw.replace("Z", "+00:00")
|
||||
return datetime.fromisoformat(normalized)
|
||||
except (ValueError, TypeError):
|
||||
logger.debug("Failed to parse WATA datetime: %s", raw)
|
||||
return None
|
||||
|
||||
async def create_payment_link(
|
||||
self,
|
||||
*,
|
||||
amount_kopeks: int,
|
||||
currency: str,
|
||||
description: str,
|
||||
order_id: str,
|
||||
success_url: Optional[str] = None,
|
||||
fail_url: Optional[str] = None,
|
||||
link_type: Optional[str] = None,
|
||||
expiration_minutes: Optional[int] = None,
|
||||
allow_arbitrary_amount: bool = False,
|
||||
arbitrary_amount_prompts: Optional[list[int]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
payload: Dict[str, Any] = {
|
||||
"amount": self._amount_from_kopeks(amount_kopeks),
|
||||
"currency": currency,
|
||||
"description": description,
|
||||
"orderId": order_id,
|
||||
}
|
||||
|
||||
payload["type"] = link_type or settings.WATA_PAYMENT_TYPE or "OneTime"
|
||||
|
||||
if success_url or settings.WATA_SUCCESS_REDIRECT_URL:
|
||||
payload["successRedirectUrl"] = success_url or settings.WATA_SUCCESS_REDIRECT_URL
|
||||
if fail_url or settings.WATA_FAIL_REDIRECT_URL:
|
||||
payload["failRedirectUrl"] = fail_url or settings.WATA_FAIL_REDIRECT_URL
|
||||
|
||||
if expiration_minutes is None:
|
||||
ttl = settings.WATA_LINK_TTL_MINUTES
|
||||
expiration_minutes = int(ttl) if ttl is not None else None
|
||||
|
||||
if expiration_minutes:
|
||||
expiration_time = datetime.utcnow() + timedelta(minutes=expiration_minutes)
|
||||
payload["expirationDateTime"] = self._format_datetime(expiration_time)
|
||||
|
||||
if allow_arbitrary_amount:
|
||||
payload["isArbitraryAmountAllowed"] = True
|
||||
if arbitrary_amount_prompts:
|
||||
payload["arbitraryAmountPrompts"] = arbitrary_amount_prompts
|
||||
|
||||
logger.info(
|
||||
"Создаем WATA платежную ссылку: order_id=%s, amount=%s %s",
|
||||
order_id,
|
||||
payload["amount"],
|
||||
currency,
|
||||
)
|
||||
|
||||
response = await self._request("POST", "/links", json=payload)
|
||||
logger.debug("WATA create link response: %s", response)
|
||||
return response
|
||||
|
||||
async def get_payment_link(self, payment_link_id: str) -> Dict[str, Any]:
|
||||
logger.debug("Запрашиваем WATA ссылку %s", payment_link_id)
|
||||
return await self._request("GET", f"/links/{payment_link_id}")
|
||||
|
||||
async def search_transactions(
|
||||
self,
|
||||
*,
|
||||
order_id: Optional[str] = None,
|
||||
payment_link_id: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
limit: int = 5,
|
||||
) -> Dict[str, Any]:
|
||||
params: Dict[str, Any] = {
|
||||
"skipCount": 0,
|
||||
"maxResultCount": max(1, min(limit, 1000)),
|
||||
}
|
||||
if order_id:
|
||||
params["orderId"] = order_id
|
||||
if status:
|
||||
params["statuses"] = status
|
||||
if payment_link_id:
|
||||
params["paymentLinkId"] = payment_link_id
|
||||
|
||||
logger.debug(
|
||||
"Ищем WATA транзакции: order_id=%s, payment_link_id=%s", order_id, payment_link_id
|
||||
)
|
||||
return await self._request("GET", "/transactions", params=params)
|
||||
|
||||
async def get_transaction(self, transaction_id: str) -> Dict[str, Any]:
|
||||
logger.debug("Получаем WATA транзакцию %s", transaction_id)
|
||||
return await self._request("GET", f"/transactions/{transaction_id}")
|
||||
@@ -54,6 +54,15 @@ def get_available_payment_methods() -> List[Dict[str, str]]:
|
||||
"callback": "topup_mulenpay"
|
||||
})
|
||||
|
||||
if settings.is_wata_enabled():
|
||||
methods.append({
|
||||
"id": "wata",
|
||||
"name": "Банковская карта",
|
||||
"icon": "💳",
|
||||
"description": "через WATA",
|
||||
"callback": "topup_wata"
|
||||
})
|
||||
|
||||
if settings.is_pal24_enabled():
|
||||
methods.append({
|
||||
"id": "pal24",
|
||||
@@ -141,6 +150,8 @@ def is_payment_method_available(method_id: str) -> bool:
|
||||
return settings.TRIBUTE_ENABLED
|
||||
elif method_id == "mulenpay":
|
||||
return settings.is_mulenpay_enabled()
|
||||
elif method_id == "wata":
|
||||
return settings.is_wata_enabled()
|
||||
elif method_id == "pal24":
|
||||
return settings.is_pal24_enabled()
|
||||
elif method_id == "cryptobot":
|
||||
@@ -159,6 +170,7 @@ def get_payment_method_status() -> Dict[str, bool]:
|
||||
"yookassa": settings.is_yookassa_enabled(),
|
||||
"tribute": settings.TRIBUTE_ENABLED,
|
||||
"mulenpay": settings.is_mulenpay_enabled(),
|
||||
"wata": settings.is_wata_enabled(),
|
||||
"pal24": settings.is_pal24_enabled(),
|
||||
"cryptobot": settings.is_cryptobot_enabled(),
|
||||
"support": True
|
||||
@@ -177,6 +189,8 @@ def get_enabled_payment_methods_count() -> int:
|
||||
count += 1
|
||||
if settings.is_mulenpay_enabled():
|
||||
count += 1
|
||||
if settings.is_wata_enabled():
|
||||
count += 1
|
||||
if settings.is_pal24_enabled():
|
||||
count += 1
|
||||
if settings.is_cryptobot_enabled():
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
"PAYMENTS_TEMPORARILY_UNAVAILABLE": "⚠️ Payment methods are temporarily unavailable",
|
||||
"PAYMENT_CARD_TRIBUTE": "💳 Bank card (Tribute)",
|
||||
"PAYMENT_CARD_MULENPAY": "💳 Bank card (Mulen Pay)",
|
||||
"PAYMENT_CARD_WATA": "💳 Bank card (WATA)",
|
||||
"PAYMENT_CARD_PAL24": "🏦 SBP (PayPalych)",
|
||||
"PAYMENT_CARD_YOOKASSA": "💳 Bank card (YooKassa)",
|
||||
"PAYMENT_CRYPTOBOT": "🪙 Cryptocurrency (CryptoBot)",
|
||||
@@ -86,6 +87,10 @@
|
||||
"MULENPAY_PAYMENT_ERROR": "❌ Failed to create Mulen Pay payment. Please try again later or contact support.",
|
||||
"MULENPAY_PAY_BUTTON": "💳 Pay with Mulen Pay",
|
||||
"MULENPAY_PAYMENT_INSTRUCTIONS": "💳 <b>Mulen Pay payment</b>\n\n💰 Amount: {amount}\n🆔 Payment ID: {payment_id}\n\n📱 <b>How to pay:</b>\n1. Press ‘Pay with Mulen Pay’\n2. Follow the instructions on the payment page\n3. Confirm the transfer\n4. Funds will be credited automatically\n\n❓ Need help? Contact {support}",
|
||||
"WATA_TOPUP_PROMPT": "💳 <b>Pay via WATA</b>\n\nEnter the amount to top up. Minimum amount — {min_amount}, maximum — {max_amount}.\nPayments are processed through the secure WATA form.",
|
||||
"WATA_PAYMENT_ERROR": "❌ Failed to create WATA payment. Please try again later or contact support.",
|
||||
"WATA_PAY_BUTTON": "💳 Pay via WATA",
|
||||
"WATA_PAYMENT_INSTRUCTIONS": "💳 <b>Pay via WATA</b>\n\n💰 Amount: {amount}\n🆔 Payment ID: {payment_id}\n\n📱 <b>How to pay:</b>\n1. Tap ‘Pay via WATA’\n2. Follow the gateway prompts\n3. Confirm the transfer\n4. Funds will be credited automatically\n\n❓ Need help? Contact {support}",
|
||||
"PAL24_TOPUP_PROMPT": "🏦 <b>PayPalych (SBP) payment</b>\n\nEnter an amount between 100 and 1,000,000 ₽.\nThe payment is processed via the PayPalych Faster Payments System.",
|
||||
"PAL24_PAYMENT_ERROR": "❌ Failed to create a PayPalych payment. Please try again later or contact support.",
|
||||
"PAL24_PAY_BUTTON": "🏦 Pay with PayPalych (SBP)",
|
||||
@@ -96,6 +101,14 @@
|
||||
"PAL24_INSTRUCTION_FOLLOW": "{step}. Follow the payment page instructions",
|
||||
"PAL24_INSTRUCTION_CONFIRM": "{step}. Confirm the transfer",
|
||||
"PAL24_INSTRUCTION_COMPLETE": "{step}. The funds will be credited automatically",
|
||||
"WATA_AMOUNT_TOO_LOW": "Minimum top-up amount: {amount}",
|
||||
"WATA_AMOUNT_TOO_HIGH": "Maximum top-up amount: {amount}",
|
||||
"WATA_STATUS_TITLE": "💳 <b>WATA payment status</b>",
|
||||
"WATA_STATUS_OPENED": "Waiting for payment",
|
||||
"WATA_STATUS_CLOSED": "Processing",
|
||||
"WATA_STATUS_PAID": "Paid",
|
||||
"WATA_STATUS_DECLINED": "Declined",
|
||||
"WATA_STATUS_UNKNOWN": "Unknown",
|
||||
"PENDING_CANCEL_BUTTON": "⌛ Cancel",
|
||||
"POST_REGISTRATION_TRIAL_BUTTON": "🚀 Activate free trial 🚀",
|
||||
"REFERRAL_ANALYTICS_BUTTON": "📊 Analytics",
|
||||
@@ -564,6 +577,8 @@
|
||||
"PAYMENT_METHOD_TRIBUTE_DESCRIPTION": "via Tribute",
|
||||
"PAYMENT_METHOD_MULENPAY_NAME": "💳 <b>Bank card (Mulen Pay)</b>",
|
||||
"PAYMENT_METHOD_MULENPAY_DESCRIPTION": "via Mulen Pay",
|
||||
"PAYMENT_METHOD_WATA_NAME": "💳 <b>Bank card (WATA)</b>",
|
||||
"PAYMENT_METHOD_WATA_DESCRIPTION": "via WATA",
|
||||
"PAYMENT_METHOD_PAL24_NAME": "🏦 <b>SBP (PayPalych)</b>",
|
||||
"PAYMENT_METHOD_PAL24_DESCRIPTION": "via Faster Payments System",
|
||||
"PAYMENT_METHOD_CRYPTOBOT_NAME": "🪙 <b>Cryptocurrency</b>",
|
||||
|
||||
@@ -292,6 +292,7 @@
|
||||
"PAYMENTS_TEMPORARILY_UNAVAILABLE": "⚠️ Способы оплаты временно недоступны",
|
||||
"PAYMENT_CARD_TRIBUTE": "💳 Банковская карта (Tribute)",
|
||||
"PAYMENT_CARD_MULENPAY": "💳 Банковская карта (Mulen Pay)",
|
||||
"PAYMENT_CARD_WATA": "💳 Банковская карта (WATA)",
|
||||
"PAYMENT_CARD_PAL24": "🏦 СБП (PayPalych)",
|
||||
"PAYMENT_CARD_YOOKASSA": "💳 Банковская карта (YooKassa)",
|
||||
"PAYMENT_CRYPTOBOT": "🪙 Криптовалюта (CryptoBot)",
|
||||
@@ -304,6 +305,10 @@
|
||||
"MULENPAY_PAYMENT_ERROR": "❌ Ошибка создания платежа Mulen Pay. Попробуйте позже или обратитесь в поддержку.",
|
||||
"MULENPAY_PAY_BUTTON": "💳 Оплатить через Mulen Pay",
|
||||
"MULENPAY_PAYMENT_INSTRUCTIONS": "💳 <b>Оплата через Mulen Pay</b>\n\n💰 Сумма: {amount}\n🆔 ID платежа: {payment_id}\n\n📱 <b>Инструкция:</b>\n1. Нажмите кнопку ‘Оплатить через Mulen Pay’\n2. Следуйте подсказкам платежной системы\n3. Подтвердите перевод\n4. Средства зачислятся автоматически\n\n❓ Если возникнут проблемы, обратитесь в {support}",
|
||||
"WATA_TOPUP_PROMPT": "💳 <b>Оплата через WATA</b>\n\nВведите сумму пополнения. Минимальная сумма — {min_amount}, максимальная — {max_amount}.\nОплата происходит через защищенную форму WATA.",
|
||||
"WATA_PAYMENT_ERROR": "❌ Ошибка создания платежа WATA. Попробуйте позже или обратитесь в поддержку.",
|
||||
"WATA_PAY_BUTTON": "💳 Оплатить через WATA",
|
||||
"WATA_PAYMENT_INSTRUCTIONS": "💳 <b>Оплата через WATA</b>\n\n💰 Сумма: {amount}\n🆔 ID платежа: {payment_id}\n\n📱 <b>Инструкция:</b>\n1. Нажмите кнопку 'Оплатить через WATA'\n2. Следуйте подсказкам платежной системы\n3. Подтвердите перевод\n4. Средства зачислятся автоматически\n\n❓ Если возникнут проблемы, обратитесь в {support}",
|
||||
"PAL24_TOPUP_PROMPT": "🏦 <b>Оплата через PayPalych (СБП)</b>\n\nВведите сумму для пополнения от 100 до 1 000 000 ₽.\nОплата проходит через систему быстрых платежей PayPalych.",
|
||||
"PAL24_PAYMENT_ERROR": "❌ Ошибка создания платежа PayPalych. Попробуйте позже или обратитесь в поддержку.",
|
||||
"PAL24_PAY_BUTTON": "🏦 Оплатить через PayPalych (СБП)",
|
||||
@@ -314,6 +319,14 @@
|
||||
"PAL24_INSTRUCTION_FOLLOW": "{step}. Следуйте подсказкам платёжной системы",
|
||||
"PAL24_INSTRUCTION_CONFIRM": "{step}. Подтвердите перевод",
|
||||
"PAL24_INSTRUCTION_COMPLETE": "{step}. Средства зачислятся автоматически",
|
||||
"WATA_AMOUNT_TOO_LOW": "Минимальная сумма пополнения: {amount}",
|
||||
"WATA_AMOUNT_TOO_HIGH": "Максимальная сумма пополнения: {amount}",
|
||||
"WATA_STATUS_TITLE": "💳 <b>Статус платежа WATA</b>",
|
||||
"WATA_STATUS_OPENED": "Ожидает оплаты",
|
||||
"WATA_STATUS_CLOSED": "Обрабатывается",
|
||||
"WATA_STATUS_PAID": "Оплачен",
|
||||
"WATA_STATUS_DECLINED": "Отклонен",
|
||||
"WATA_STATUS_UNKNOWN": "Неизвестно",
|
||||
"PENDING_CANCEL_BUTTON": "⌛ Отмена",
|
||||
"PERIOD_14_DAYS": "📅 14 дней - {settings.format_price(settings.PRICE_14_DAYS)}",
|
||||
"PERIOD_180_DAYS": "📅 180 дней - {settings.format_price(settings.PRICE_180_DAYS)}",
|
||||
@@ -566,6 +579,8 @@
|
||||
"PAYMENT_METHOD_TRIBUTE_DESCRIPTION": "через Tribute",
|
||||
"PAYMENT_METHOD_MULENPAY_NAME": "💳 <b>Банковская карта (Mulen Pay)</b>",
|
||||
"PAYMENT_METHOD_MULENPAY_DESCRIPTION": "через Mulen Pay",
|
||||
"PAYMENT_METHOD_WATA_NAME": "💳 <b>Банковская карта (WATA)</b>",
|
||||
"PAYMENT_METHOD_WATA_DESCRIPTION": "через WATA",
|
||||
"PAYMENT_METHOD_PAL24_NAME": "🏦 <b>СБП (PayPalych)</b>",
|
||||
"PAYMENT_METHOD_PAL24_DESCRIPTION": "через систему быстрых платежей",
|
||||
"PAYMENT_METHOD_CRYPTOBOT_NAME": "🪙 <b>Криптовалюта</b>",
|
||||
|
||||
@@ -17,6 +17,7 @@ from app.services.payment import ( # noqa: E402
|
||||
TelegramStarsMixin,
|
||||
TributePaymentMixin,
|
||||
YooKassaPaymentMixin,
|
||||
WataPaymentMixin,
|
||||
)
|
||||
from app.services.payment_service import PaymentService # noqa: E402
|
||||
|
||||
@@ -31,6 +32,7 @@ def test_payment_service_mro_contains_all_mixins() -> None:
|
||||
CryptoBotPaymentMixin,
|
||||
MulenPayPaymentMixin,
|
||||
Pal24PaymentMixin,
|
||||
WataPaymentMixin,
|
||||
}
|
||||
service_mro = set(PaymentService.__mro__)
|
||||
assert mixins.issubset(service_mro), "PaymentService должен содержать все mixin-классы"
|
||||
@@ -46,6 +48,7 @@ def test_payment_service_mro_contains_all_mixins() -> None:
|
||||
"create_cryptobot_payment",
|
||||
"create_mulenpay_payment",
|
||||
"create_pal24_payment",
|
||||
"create_wata_payment",
|
||||
],
|
||||
)
|
||||
def test_payment_service_exposes_provider_methods(attribute: str) -> None:
|
||||
|
||||
144
tests/services/test_payment_service_wata.py
Normal file
144
tests/services/test_payment_service_wata.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""Tests for WATA payment mixin."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT_DIR = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT_DIR))
|
||||
|
||||
import app.services.payment_service as payment_service_module # noqa: E402
|
||||
from app.config import settings # noqa: E402
|
||||
from app.services.payment_service import PaymentService # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def anyio_backend() -> str:
|
||||
return "asyncio"
|
||||
|
||||
|
||||
class DummySession:
|
||||
async def commit(self) -> None: # pragma: no cover - no logic required
|
||||
return None
|
||||
|
||||
async def refresh(self, *_: Any) -> None: # pragma: no cover - no logic required
|
||||
return None
|
||||
|
||||
|
||||
class DummyLocalPayment:
|
||||
def __init__(self, payment_id: int = 42) -> None:
|
||||
self.id = payment_id
|
||||
self.created_at = datetime.utcnow()
|
||||
|
||||
|
||||
class StubWataService:
|
||||
def __init__(self, response: Optional[Dict[str, Any]]) -> None:
|
||||
self.response = response
|
||||
self.calls: list[Dict[str, Any]] = []
|
||||
|
||||
async def create_payment_link(self, **kwargs: Any) -> Optional[Dict[str, Any]]:
|
||||
self.calls.append(kwargs)
|
||||
return self.response
|
||||
|
||||
|
||||
def _make_service(stub: Optional[StubWataService]) -> PaymentService:
|
||||
service = PaymentService.__new__(PaymentService) # type: ignore[call-arg]
|
||||
service.bot = None
|
||||
service.wata_service = stub
|
||||
service.mulenpay_service = None
|
||||
service.pal24_service = None
|
||||
service.yookassa_service = None
|
||||
service.stars_service = None
|
||||
service.cryptobot_service = None
|
||||
return service
|
||||
|
||||
|
||||
@pytest.mark.anyio("asyncio")
|
||||
async def test_create_wata_payment_success(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
response = {
|
||||
"id": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"url": "https://wata.example/link",
|
||||
"status": "Opened",
|
||||
"type": "OneTime",
|
||||
"terminalPublicId": "terminal-id",
|
||||
"successRedirectUrl": "https://example.com/success",
|
||||
"failRedirectUrl": "https://example.com/fail",
|
||||
"expirationDateTime": "2030-01-01T00:00:00Z",
|
||||
}
|
||||
stub = StubWataService(response)
|
||||
service = _make_service(stub)
|
||||
db = DummySession()
|
||||
|
||||
captured_args: Dict[str, Any] = {}
|
||||
|
||||
async def fake_create_wata_payment(**kwargs: Any) -> DummyLocalPayment:
|
||||
captured_args.update(kwargs)
|
||||
return DummyLocalPayment(payment_id=777)
|
||||
|
||||
monkeypatch.setattr(payment_service_module, "create_wata_payment", fake_create_wata_payment, raising=False)
|
||||
monkeypatch.setattr(settings, "WATA_MIN_AMOUNT_KOPEKS", 5000, raising=False)
|
||||
monkeypatch.setattr(settings, "WATA_MAX_AMOUNT_KOPEKS", 500_000, raising=False)
|
||||
|
||||
result = await service.create_wata_payment(
|
||||
db=db,
|
||||
user_id=101,
|
||||
amount_kopeks=15000,
|
||||
description="Пополнение",
|
||||
language="ru",
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert result["local_payment_id"] == 777
|
||||
assert result["payment_link_id"] == response["id"]
|
||||
assert result["payment_url"] == response["url"]
|
||||
assert captured_args["user_id"] == 101
|
||||
assert captured_args["amount_kopeks"] == 15000
|
||||
assert captured_args["payment_link_id"] == response["id"]
|
||||
assert stub.calls and stub.calls[0]["amount_kopeks"] == 15000
|
||||
|
||||
|
||||
@pytest.mark.anyio("asyncio")
|
||||
async def test_create_wata_payment_respects_amount_limits(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
stub = StubWataService({"id": "link"})
|
||||
service = _make_service(stub)
|
||||
db = DummySession()
|
||||
|
||||
monkeypatch.setattr(settings, "WATA_MIN_AMOUNT_KOPEKS", 10_000, raising=False)
|
||||
monkeypatch.setattr(settings, "WATA_MAX_AMOUNT_KOPEKS", 20_000, raising=False)
|
||||
|
||||
too_low = await service.create_wata_payment(
|
||||
db=db,
|
||||
user_id=1,
|
||||
amount_kopeks=5_000,
|
||||
description="Пополнение",
|
||||
)
|
||||
assert too_low is None
|
||||
|
||||
too_high = await service.create_wata_payment(
|
||||
db=db,
|
||||
user_id=1,
|
||||
amount_kopeks=25_000,
|
||||
description="Пополнение",
|
||||
)
|
||||
assert too_high is None
|
||||
assert not stub.calls
|
||||
|
||||
|
||||
@pytest.mark.anyio("asyncio")
|
||||
async def test_create_wata_payment_returns_none_without_service() -> None:
|
||||
service = _make_service(None)
|
||||
db = DummySession()
|
||||
|
||||
result = await service.create_wata_payment(
|
||||
db=db,
|
||||
user_id=5,
|
||||
amount_kopeks=10_000,
|
||||
description="Пополнение",
|
||||
)
|
||||
assert result is None
|
||||
Reference in New Issue
Block a user