diff --git a/app/config.py b/app/config.py index b200a446..a7e80bba 100644 --- a/app/config.py +++ b/app/config.py @@ -213,20 +213,6 @@ class Settings(BaseSettings): MULENPAY_MIN_AMOUNT_KOPEKS: int = 10000 MULENPAY_MAX_AMOUNT_KOPEKS: int = 10000000 - WATA_ENABLED: bool = False - WATA_ACCESS_TOKEN: Optional[str] = None - WATA_BASE_URL: str = "https://api.wata.pro/api/h2h" - WATA_WEBHOOK_PATH: str = "/wata-webhook" - WATA_DESCRIPTION: str = "Пополнение баланса" - WATA_DEFAULT_CURRENCY: str = "RUB" - WATA_SUCCESS_REDIRECT_URL: Optional[str] = None - WATA_FAIL_REDIRECT_URL: Optional[str] = None - WATA_LINK_TYPE: str = "OneTime" - WATA_ALLOW_ARBITRARY_AMOUNT: bool = False - WATA_MIN_AMOUNT_KOPEKS: int = 10000 - WATA_MAX_AMOUNT_KOPEKS: int = 100000000 - WATA_TIMEOUT_SECONDS: int = 60 - PAL24_ENABLED: bool = False PAL24_API_TOKEN: Optional[str] = None PAL24_SHOP_ID: Optional[str] = None @@ -734,9 +720,6 @@ class Settings(BaseSettings): and self.MULENPAY_SHOP_ID is not None ) - def is_wata_enabled(self) -> bool: - return bool(self.WATA_ENABLED and self.WATA_ACCESS_TOKEN) - def is_pal24_enabled(self) -> bool: return ( self.PAL24_ENABLED diff --git a/app/database/crud/wata.py b/app/database/crud/wata.py deleted file mode 100644 index dd7b4563..00000000 --- a/app/database/crud/wata.py +++ /dev/null @@ -1,139 +0,0 @@ -"""CRUD-операции для платежей Wata Pay.""" - -from __future__ import annotations - -import logging -from datetime import datetime -from typing import Optional - -from sqlalchemy import select -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, - amount_kopeks: int, - order_id: str, - description: Optional[str], - status: str, - currency: str, - payment_url: Optional[str], - wata_link_id: Optional[str], - success_redirect_url: Optional[str], - fail_redirect_url: Optional[str], - expiration_at: Optional[datetime], - metadata: Optional[dict] = None, -) -> WataPayment: - payment = WataPayment( - user_id=user_id, - amount_kopeks=amount_kopeks, - order_id=order_id, - description=description, - status=status, - currency=currency, - payment_url=payment_url, - wata_link_id=wata_link_id, - success_redirect_url=success_redirect_url, - fail_redirect_url=fail_redirect_url, - expiration_at=expiration_at, - metadata_json=metadata or {}, - ) - - db.add(payment) - await db.commit() - await db.refresh(payment) - - logger.info( - "Создан Wata платеж #%s (order=%s) на сумму %s копеек для пользователя %s", - payment.id, - order_id, - amount_kopeks, - user_id, - ) - - return payment - - -async def get_wata_payment_by_local_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_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 get_wata_payment_by_link_id( - db: AsyncSession, - link_id: str, -) -> Optional[WataPayment]: - result = await db.execute( - select(WataPayment).where(WataPayment.wata_link_id == link_id) - ) - return result.scalar_one_or_none() - - -async def update_wata_payment_status( - db: AsyncSession, - *, - payment: WataPayment, - status: Optional[str] = None, - transaction_status: Optional[str] = None, - is_paid: Optional[bool] = None, - paid_at: Optional[datetime] = None, - callback_payload: Optional[dict] = None, - external_transaction_id: Optional[str] = None, - payment_url: Optional[str] = None, - last_status_payload: Optional[dict] = None, -) -> WataPayment: - if status is not None: - payment.status = status - if transaction_status is not None: - payment.transaction_status = transaction_status - if is_paid is not None: - payment.is_paid = is_paid - if paid_at is not None: - payment.paid_at = paid_at - if callback_payload is not None: - payment.callback_payload = callback_payload - if external_transaction_id is not None: - payment.external_transaction_id = external_transaction_id - if payment_url is not None: - payment.payment_url = payment_url - if last_status_payload is not None: - payment.last_status_payload = last_status_payload - - payment.updated_at = datetime.utcnow() - await db.commit() - await db.refresh(payment) - return payment - - -async def link_wata_payment_to_transaction( - db: AsyncSession, - *, - payment: WataPayment, - transaction_id: int, -) -> WataPayment: - payment.transaction_id = transaction_id - payment.updated_at = datetime.utcnow() - await db.commit() - await db.refresh(payment) - return payment diff --git a/app/database/models.py b/app/database/models.py index 25dc9f19..75dca572 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -77,7 +77,6 @@ class PaymentMethod(Enum): CRYPTOBOT = "cryptobot" MULENPAY = "mulenpay" PAL24 = "pal24" - WATA = "wata" MANUAL = "manual" @@ -294,62 +293,6 @@ 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) - - wata_link_id = Column(String(64), unique=True, nullable=True, index=True) - order_id = Column(String(255), unique=True, nullable=False, index=True) - amount_kopeks = Column(Integer, nullable=False) - currency = Column(String(10), nullable=False, default="RUB") - description = Column(Text, nullable=True) - - status = Column(String(32), nullable=False, default="Opened") - transaction_status = Column(String(32), nullable=True) - external_transaction_id = Column(String(255), nullable=True, index=True) - - payment_url = Column(Text, nullable=True) - success_redirect_url = Column(Text, nullable=True) - fail_redirect_url = Column(Text, nullable=True) - expiration_at = Column(DateTime, nullable=True) - - is_paid = Column(Boolean, default=False) - paid_at = Column(DateTime, nullable=True) - - metadata_json = Column(JSON, nullable=True) - callback_payload = Column(JSON, nullable=True) - last_status_payload = Column(JSON, 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 - - @property - def is_pending(self) -> bool: - return not self.is_paid and (self.status or "").lower() == "opened" - - def __repr__(self) -> str: # pragma: no cover - debug helper - return ( - "".format( - self.id, - self.order_id, - self.amount_rubles, - self.status, - self.transaction_status, - ) - ) - - class PromoGroup(Base): __tablename__ = "promo_groups" diff --git a/app/database/universal_migration.py b/app/database/universal_migration.py index d1de5476..587095a8 100644 --- a/app/database/universal_migration.py +++ b/app/database/universal_migration.py @@ -2876,137 +2876,6 @@ async def ensure_default_web_api_token() -> bool: 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, - wata_link_id VARCHAR(64) NULL UNIQUE, - order_id VARCHAR(255) NOT NULL UNIQUE, - amount_kopeks INTEGER NOT NULL, - currency VARCHAR(10) NOT NULL DEFAULT 'RUB', - description TEXT NULL, - status VARCHAR(32) NOT NULL DEFAULT 'Opened', - transaction_status VARCHAR(32) NULL, - external_transaction_id VARCHAR(255) NULL, - payment_url TEXT NULL, - success_redirect_url TEXT NULL, - fail_redirect_url TEXT NULL, - expiration_at DATETIME NULL, - is_paid BOOLEAN NOT NULL DEFAULT 0, - paid_at DATETIME NULL, - metadata_json JSON NULL, - callback_payload JSON NULL, - last_status_payload JSON 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_external_transaction_id - ON wata_payments(external_transaction_id); - - CREATE INDEX idx_wata_user_id - ON wata_payments(user_id); - """ - - elif db_type == 'postgresql': - create_sql = """ - CREATE TABLE wata_payments ( - id SERIAL PRIMARY KEY, - user_id INTEGER NOT NULL REFERENCES users(id), - wata_link_id VARCHAR(64) NULL UNIQUE, - order_id VARCHAR(255) NOT NULL UNIQUE, - amount_kopeks INTEGER NOT NULL, - currency VARCHAR(10) NOT NULL DEFAULT 'RUB', - description TEXT NULL, - status VARCHAR(32) NOT NULL DEFAULT 'Opened', - transaction_status VARCHAR(32) NULL, - external_transaction_id VARCHAR(255) NULL, - payment_url TEXT NULL, - success_redirect_url TEXT NULL, - fail_redirect_url TEXT NULL, - expiration_at TIMESTAMP NULL, - is_paid BOOLEAN NOT NULL DEFAULT FALSE, - paid_at TIMESTAMP NULL, - metadata_json JSON NULL, - callback_payload JSON NULL, - last_status_payload JSON NULL, - transaction_id INTEGER NULL REFERENCES transactions(id), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ); - - CREATE INDEX idx_wata_external_transaction_id - ON wata_payments(external_transaction_id); - - CREATE INDEX idx_wata_user_id - ON wata_payments(user_id); - """ - - elif db_type == 'mysql': - create_sql = """ - CREATE TABLE wata_payments ( - id INT AUTO_INCREMENT PRIMARY KEY, - user_id INT NOT NULL, - wata_link_id VARCHAR(64) NULL UNIQUE, - order_id VARCHAR(255) NOT NULL UNIQUE, - amount_kopeks INT NOT NULL, - currency VARCHAR(10) NOT NULL DEFAULT 'RUB', - description TEXT NULL, - status VARCHAR(32) NOT NULL DEFAULT 'Opened', - transaction_status VARCHAR(32) NULL, - external_transaction_id VARCHAR(255) NULL, - payment_url TEXT NULL, - success_redirect_url TEXT NULL, - fail_redirect_url TEXT NULL, - expiration_at DATETIME NULL, - is_paid BOOLEAN NOT NULL DEFAULT 0, - paid_at DATETIME NULL, - metadata_json JSON NULL, - callback_payload JSON NULL, - last_status_payload JSON 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_external_transaction_id - ON wata_payments(external_transaction_id); - - CREATE INDEX idx_wata_user_id - ON wata_payments(user_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 run_universal_migration(): logger.info("=== НАЧАЛО УНИВЕРСАЛЬНОЙ МИГРАЦИИ ===") @@ -3109,13 +2978,6 @@ async def run_universal_migration(): else: logger.warning("⚠️ Проблемы с таблицей Pal24 payments") - logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ WATA PAYMENTS ===") - wata_created = await create_wata_payments_table() - if wata_created: - logger.info("✅ Таблица Wata payments готова") - else: - logger.warning("⚠️ Проблемы с таблицей Wata payments") - logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ DISCOUNT_OFFERS ===") discount_created = await create_discount_offers_table() if discount_created: diff --git a/app/external/webhook_server.py b/app/external/webhook_server.py index f3e08e53..99531c59 100644 --- a/app/external/webhook_server.py +++ b/app/external/webhook_server.py @@ -36,9 +36,6 @@ class WebhookServer: if settings.is_cryptobot_enabled(): self.app.router.add_post(settings.CRYPTOBOT_WEBHOOK_PATH, self._cryptobot_webhook_handler) - - if settings.is_wata_enabled(): - self.app.router.add_post(settings.WATA_WEBHOOK_PATH, self._wata_webhook_handler) self.app.router.add_get('/health', self._health_check) @@ -47,8 +44,6 @@ class WebhookServer: self.app.router.add_options(settings.MULENPAY_WEBHOOK_PATH, self._options_handler) if settings.is_cryptobot_enabled(): self.app.router.add_options(settings.CRYPTOBOT_WEBHOOK_PATH, self._options_handler) - if settings.is_wata_enabled(): - self.app.router.add_options(settings.WATA_WEBHOOK_PATH, self._options_handler) logger.info(f"Webhook сервер настроен:") logger.info(f" - Tribute webhook: POST {settings.TRIBUTE_WEBHOOK_PATH}") @@ -56,8 +51,6 @@ class WebhookServer: logger.info(f" - Mulen Pay webhook: POST {settings.MULENPAY_WEBHOOK_PATH}") if settings.is_cryptobot_enabled(): logger.info(f" - CryptoBot webhook: POST {settings.CRYPTOBOT_WEBHOOK_PATH}") - if settings.is_wata_enabled(): - logger.info(f" - Wata Pay webhook: POST {settings.WATA_WEBHOOK_PATH}") logger.info(f" - Health check: GET /health") return self.app @@ -104,13 +97,6 @@ class WebhookServer: settings.TRIBUTE_WEBHOOK_PORT, settings.CRYPTOBOT_WEBHOOK_PATH, ) - if settings.is_wata_enabled(): - logger.info( - "Wata webhook URL: http://%s:%s%s", - settings.TRIBUTE_WEBHOOK_HOST, - settings.TRIBUTE_WEBHOOK_PORT, - settings.WATA_WEBHOOK_PATH, - ) except Exception as e: logger.error(f"Ошибка запуска webhook сервера: {e}") @@ -136,7 +122,7 @@ class WebhookServer: headers={ 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type, trbt-signature, Crypto-Pay-API-Signature, X-MulenPay-Signature, X-Signature, Authorization', + 'Access-Control-Allow-Headers': 'Content-Type, trbt-signature, Crypto-Pay-API-Signature, X-MulenPay-Signature, Authorization', } ) @@ -186,53 +172,6 @@ class WebhookServer: logger.error(f"Критическая ошибка Mulen Pay webhook: {error}", exc_info=True) return web.json_response({"status": "error", "reason": "internal_error", "message": str(error)}, status=500) - async def _wata_webhook_handler(self, request: web.Request) -> web.Response: - try: - logger.info(f"Получен Wata webhook: {request.method} {request.path}") - raw_body = await request.read() - - if not raw_body: - logger.warning("Пустой Wata webhook") - return web.json_response({"status": "error", "reason": "empty_body"}, status=400) - - signature = request.headers.get("X-Signature", "") - - payment_service = PaymentService(self.bot) - is_valid = await payment_service.verify_wata_webhook_signature(raw_body, signature) - if not is_valid: - logger.warning("Подпись Wata webhook не прошла проверку") - return web.json_response( - {"status": "accepted", "reason": "invalid_signature"}, - status=202, - ) - - try: - payload = json.loads(raw_body.decode("utf-8")) - except json.JSONDecodeError as error: - logger.error(f"Ошибка парсинга Wata webhook: {error}") - return web.json_response({"status": "error", "reason": "invalid_json"}, status=400) - - db_generator = get_db() - db = await db_generator.__anext__() - - try: - processed = await payment_service.process_wata_webhook(db, payload) - if processed: - return web.json_response({"status": "ok"}, status=200) - return web.json_response({"status": "error", "reason": "processing_failed"}, status=400) - except Exception as error: - logger.error("Ошибка обработки Wata webhook: %s", error, exc_info=True) - return web.json_response({"status": "error", "reason": "internal_error"}, status=500) - finally: - try: - await db_generator.__anext__() - except StopAsyncIteration: - pass - - except Exception as error: - logger.error("Критическая ошибка Wata webhook: %s", error, exc_info=True) - return web.json_response({"status": "error", "reason": "internal_error", "message": str(error)}, status=500) - @staticmethod def _extract_mulenpay_header(request: web.Request, header_names: Iterable[str]) -> Optional[str]: for header_name in header_names: diff --git a/app/handlers/balance.py b/app/handlers/balance.py index 9706f010..a721f75a 100644 --- a/app/handlers/balance.py +++ b/app/handlers/balance.py @@ -372,45 +372,6 @@ async def start_mulenpay_payment( await callback.answer() -@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 Pay временно недоступна", show_alert=True) - return - - message_text = texts.t( - "WATA_TOPUP_PROMPT", - ( - "🌐 Оплата через Wata Pay\n\n" - "Введите сумму для пополнения от 100 до 1 000 000 ₽.\n" - "Оплата проходит через платформу Wata Pay." - ), - ) - - keyboard = get_back_keyboard(db_user.language) - - if settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED: - 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 start_pal24_payment( callback: types.CallbackQuery, @@ -672,10 +633,6 @@ async def process_topup_amount( from app.database.database import AsyncSessionLocal 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 - 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 async with AsyncSessionLocal() as db: @@ -1036,118 +993,6 @@ async def process_mulenpay_payment_amount( @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 Pay временно недоступна") - return - - if amount_kopeks < settings.WATA_MIN_AMOUNT_KOPEKS: - await message.answer( - f"Минимальная сумма пополнения: {settings.format_price(settings.WATA_MIN_AMOUNT_KOPEKS)}" - ) - return - - if amount_kopeks > settings.WATA_MAX_AMOUNT_KOPEKS: - await message.answer( - f"Максимальная сумма пополнения: {settings.format_price(settings.WATA_MAX_AMOUNT_KOPEKS)}" - ) - return - - try: - payment_service = PaymentService(message.bot) - payment_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, - ) - - if not payment_result or not payment_result.get("payment_url"): - await message.answer( - texts.t( - "WATA_PAYMENT_ERROR", - "❌ Ошибка создания платежа Wata Pay. Попробуйте позже или обратитесь в поддержку.", - ) - ) - await state.clear() - return - - payment_url = payment_result["payment_url"] - local_payment_id = payment_result["local_payment_id"] - order_id = payment_result.get("order_id") or local_payment_id - - keyboard = types.InlineKeyboardMarkup( - inline_keyboard=[ - [ - types.InlineKeyboardButton( - text=texts.t("WATA_PAY_BUTTON", "🌐 Оплатить через Wata Pay"), - 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", - ( - "🌐 Оплата через Wata Pay\n\n" - "💰 Сумма: {amount}\n" - "🆔 ID платежа: {payment_id}\n\n" - "📱 Инструкция:\n" - "1. Нажмите кнопку ‘Оплатить через Wata Pay’\n" - "2. Следуйте подсказкам платежной системы\n" - "3. Подтвердите перевод\n" - "4. Средства зачислятся автоматически\n\n" - "❓ Если возникнут проблемы, обратитесь в {support}" - ), - ) - - message_text = message_template.format( - amount=settings.format_price(amount_kopeks), - payment_id=order_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₽, ID: %s", - db_user.telegram_id, - amount_kopeks / 100, - order_id, - ) - - except Exception as error: - logger.error(f"Ошибка создания Wata платежа: {error}") - await message.answer( - texts.t( - "WATA_PAYMENT_ERROR", - "❌ Ошибка создания платежа Wata Pay. Попробуйте позже или обратитесь в поддержку.", - ) - ) - await state.clear() async def process_pal24_payment_amount( message: types.Message, db_user: User, @@ -1478,146 +1323,6 @@ async def check_mulenpay_payment_status( await callback.answer("❌ Ошибка проверки статуса", show_alert=True) -@error_handler -async def check_wata_payment_status( - callback: types.CallbackQuery, - db: AsyncSession, -): - base_texts = get_texts(settings.DEFAULT_LANGUAGE) - - try: - local_payment_id = int(callback.data.split('_')[-1]) - 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( - base_texts.t( - "WATA_PAYMENT_NOT_FOUND", - "❌ Платеж не найден", - ), - show_alert=True, - ) - return - - payment = status_info["payment"] - remote_transaction = status_info.get("remote_transaction") - - user = getattr(payment, "user", None) - language = getattr(user, "language", None) or settings.DEFAULT_LANGUAGE - texts = get_texts(language) - - status_labels = { - "Opened": ( - "⏳", - texts.t("WATA_STATUS_LINK_OPENED", "Ожидает оплаты"), - ), - "Closed": ( - "✅" if payment.is_paid else "🔒", - texts.t("WATA_STATUS_LINK_CLOSED", "Ссылка закрыта"), - ), - } - transaction_labels = { - "Paid": ("✅", texts.t("WATA_STATUS_TRANSACTION_PAID", "Оплачен")), - "Declined": ("❌", texts.t("WATA_STATUS_TRANSACTION_DECLINED", "Отклонен")), - "Pending": ("⏳", texts.t("WATA_STATUS_TRANSACTION_PENDING", "В обработке")), - None: ("❓", texts.t("WATA_STATUS_TRANSACTION_UNKNOWN", "Неизвестно")), - } - - emoji, status_text = status_labels.get( - payment.status, - ("❓", texts.t("WATA_STATUS_LINK_UNKNOWN", "Неизвестно")), - ) - t_emoji, transaction_text = transaction_labels.get( - payment.transaction_status, - transaction_labels[None], - ) - - message_lines = [ - texts.t("WATA_STATUS_HEADER", "🌐 Статус платежа Wata Pay:"), - "", - texts.t("WATA_STATUS_ORDER_ID", "🆔 ID заказа: {order_id}").format( - order_id=payment.order_id, - ), - texts.t("WATA_STATUS_AMOUNT", "💰 Сумма: {amount}").format( - amount=settings.format_price(payment.amount_kopeks), - ), - texts.t("WATA_STATUS_LINK_STATUS", "📊 Статус ссылки: {emoji} {status}").format( - emoji=emoji, - status=status_text, - ), - texts.t( - "WATA_STATUS_TRANSACTION_STATUS", - "💳 Статус транзакции: {emoji} {status}", - ).format( - emoji=t_emoji, - status=transaction_text, - ), - texts.t( - "WATA_STATUS_CREATED_AT", - "📅 Создан: {created_at}", - ).format( - created_at=( - payment.created_at.strftime('%d.%m.%Y %H:%M') - if payment.created_at - else "-" - ), - ), - ] - - if payment.is_paid: - message_lines.append("") - message_lines.append( - texts.t( - "WATA_STATUS_SUCCESS", - "✅ Платеж успешно завершен! Средства уже на балансе.", - ) - ) - else: - message_lines.append("") - message_lines.append( - texts.t( - "WATA_STATUS_PENDING", - "⏳ Платеж еще не завершен. После оплаты вернитесь и проверьте статус.", - ) - ) - if payment.payment_url: - message_lines.append("") - message_lines.append( - texts.t( - "WATA_STATUS_PAYMENT_URL", - "🌐 Ссылка на оплату: {url}", - ).format(url=payment.payment_url) - ) - - if remote_transaction and not payment.is_paid: - remote_status = remote_transaction.get("status") - if remote_status: - message_lines.append("") - message_lines.append( - texts.t( - "WATA_STATUS_REMOTE_STATUS", - "ℹ️ Последний статус транзакции: {status}", - ).format(status=remote_status) - ) - - await callback.answer() - await callback.message.answer( - "\n".join(message_lines), - disable_web_page_preview=True, - ) - - except Exception as error: - logger.error(f"Ошибка проверки статуса Wata Pay: {error}") - await callback.answer( - base_texts.t( - "WATA_STATUS_ERROR", - "❌ Ошибка проверки статуса", - ), - show_alert=True, - ) - - @error_handler async def check_pal24_payment_status( callback: types.CallbackQuery, @@ -1969,12 +1674,6 @@ 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 - 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 async with AsyncSessionLocal() as db: @@ -2105,11 +1804,6 @@ def register_handlers(dp: Dispatcher): F.data == "topup_mulenpay" ) - dp.callback_query.register( - start_wata_payment, - F.data == "topup_wata" - ) - dp.callback_query.register( start_pal24_payment, F.data == "topup_pal24" @@ -2155,11 +1849,6 @@ def register_handlers(dp: Dispatcher): F.data.startswith("check_mulenpay_") ) - dp.callback_query.register( - check_wata_payment_status, - F.data.startswith("check_wata_") - ) - dp.callback_query.register( check_pal24_payment_status, F.data.startswith("check_pal24_") diff --git a/app/keyboards/inline.py b/app/keyboards/inline.py index 7f142c12..81a222c0 100644 --- a/app/keyboards/inline.py +++ b/app/keyboards/inline.py @@ -1066,14 +1066,6 @@ 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 Pay)"), - callback_data=_build_callback("wata") - ) - ]) - if settings.is_pal24_enabled(): keyboard.append([ InlineKeyboardButton( diff --git a/app/localization/locales/en.json b/app/localization/locales/en.json index 57ee57fe..7c88548c 100644 --- a/app/localization/locales/en.json +++ b/app/localization/locales/en.json @@ -600,29 +600,6 @@ "MULENPAY_PAYMENT_INSTRUCTIONS": "💳 Mulen Pay payment\n\n💰 Amount: {amount}\n🆔 Payment ID: {payment_id}\n\n📱 How to pay:\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}", "MULENPAY_PAY_BUTTON": "💳 Pay with Mulen Pay", "MULENPAY_TOPUP_PROMPT": "💳 Mulen Pay payment\n\nEnter an amount between 100 and 100,000 ₽.\nThe payment is processed by the secure Mulen Pay platform.", - "WATA_PAYMENT_ERROR": "❌ Failed to create a Wata Pay payment. Please try again later or contact support.", - "WATA_PAYMENT_INSTRUCTIONS": "🌐 Wata Pay payment\n\n💰 Amount: {amount}\n🆔 Payment ID: {payment_id}\n\n📱 How to pay:\n1. Press ‘Pay with Wata 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_PAY_BUTTON": "🌐 Pay with Wata Pay", - "WATA_TOPUP_PROMPT": "🌐 Wata Pay payment\n\nEnter an amount between 100 and 1,000,000 ₽.\nPayments are processed by the Wata Pay platform.", - "WATA_PAYMENT_NOT_FOUND": "❌ Payment not found.", - "WATA_STATUS_HEADER": "🌐 Wata Pay payment status:", - "WATA_STATUS_ORDER_ID": "🆔 Order ID: {order_id}", - "WATA_STATUS_AMOUNT": "💰 Amount: {amount}", - "WATA_STATUS_LINK_STATUS": "📊 Payment link: {emoji} {status}", - "WATA_STATUS_TRANSACTION_STATUS": "💳 Transaction: {emoji} {status}", - "WATA_STATUS_CREATED_AT": "📅 Created: {created_at}", - "WATA_STATUS_SUCCESS": "✅ Payment completed! The funds are already on your balance.", - "WATA_STATUS_PENDING": "⏳ The payment is not finished yet. Complete the payment and check the status again.", - "WATA_STATUS_PAYMENT_URL": "🌐 Payment link: {url}", - "WATA_STATUS_REMOTE_STATUS": "ℹ️ Latest transaction status: {status}", - "WATA_STATUS_LINK_OPENED": "Awaiting payment", - "WATA_STATUS_LINK_CLOSED": "Link closed", - "WATA_STATUS_LINK_UNKNOWN": "Unknown", - "WATA_STATUS_TRANSACTION_PAID": "Paid", - "WATA_STATUS_TRANSACTION_DECLINED": "Declined", - "WATA_STATUS_TRANSACTION_PENDING": "Processing", - "WATA_STATUS_TRANSACTION_UNKNOWN": "Unknown", - "WATA_STATUS_ERROR": "❌ Failed to check the status", "MY_TICKETS_BUTTON": "📋 My tickets", "MY_TICKETS_TITLE": "📋 Your tickets:", "NOTIFICATION_CLOSED": "Notification closed.", @@ -644,12 +621,9 @@ "PAL24_PAY_BUTTON": "🏦 Pay with PayPalych (SBP)", "PAL24_TOPUP_PROMPT": "🏦 PayPalych (SBP) payment\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 Pay)", "PAYMENT_CARD_PAL24": "🏦 SBP (PayPalych)", "PAYMENT_METHOD_MULENPAY_DESCRIPTION": "via Mulen Pay", "PAYMENT_METHOD_MULENPAY_NAME": "💳 Bank card (Mulen Pay)", - "PAYMENT_METHOD_WATA_DESCRIPTION": "via Wata Pay", - "PAYMENT_METHOD_WATA_NAME": "🌐 Bank card (Wata Pay)", "PAYMENT_METHOD_PAL24_DESCRIPTION": "via Faster Payments System", "PAYMENT_METHOD_PAL24_NAME": "🏦 SBP (PayPalych)", "REPLY_TO_TICKET": "💬 Reply", diff --git a/app/localization/locales/ru.json b/app/localization/locales/ru.json index 9d59e178..19f24a76 100644 --- a/app/localization/locales/ru.json +++ b/app/localization/locales/ru.json @@ -608,29 +608,6 @@ "MULENPAY_PAYMENT_INSTRUCTIONS": "💳 Оплата через Mulen Pay\n\n💰 Сумма: {amount}\n🆔 ID платежа: {payment_id}\n\n📱 Инструкция:\n1. Нажмите кнопку ‘Оплатить через Mulen Pay’\n2. Следуйте подсказкам платежной системы\n3. Подтвердите перевод\n4. Средства зачислятся автоматически\n\n❓ Если возникнут проблемы, обратитесь в {support}", "MULENPAY_PAY_BUTTON": "💳 Оплатить через Mulen Pay", "MULENPAY_TOPUP_PROMPT": "💳 Оплата через Mulen Pay\n\nВведите сумму для пополнения от 100 до 100 000 ₽.\nОплата происходит через защищенную платформу Mulen Pay.", - "WATA_PAYMENT_ERROR": "❌ Ошибка создания платежа Wata Pay. Попробуйте позже или обратитесь в поддержку.", - "WATA_PAYMENT_INSTRUCTIONS": "🌐 Оплата через Wata Pay\n\n💰 Сумма: {amount}\n🆔 ID платежа: {payment_id}\n\n📱 Инструкция:\n1. Нажмите кнопку ‘Оплатить через Wata Pay’\n2. Следуйте подсказкам платежной системы\n3. Подтвердите перевод\n4. Средства зачислятся автоматически\n\n❓ Если возникнут проблемы, обратитесь в {support}", - "WATA_PAY_BUTTON": "🌐 Оплатить через Wata Pay", - "WATA_TOPUP_PROMPT": "🌐 Оплата через Wata Pay\n\nВведите сумму для пополнения от 100 до 1 000 000 ₽.\nОплата проходит через платформу Wata Pay.", - "WATA_PAYMENT_NOT_FOUND": "❌ Платеж не найден.", - "WATA_STATUS_HEADER": "🌐 Статус платежа Wata Pay:", - "WATA_STATUS_ORDER_ID": "🆔 ID заказа: {order_id}", - "WATA_STATUS_AMOUNT": "💰 Сумма: {amount}", - "WATA_STATUS_LINK_STATUS": "📊 Статус ссылки: {emoji} {status}", - "WATA_STATUS_TRANSACTION_STATUS": "💳 Статус транзакции: {emoji} {status}", - "WATA_STATUS_CREATED_AT": "📅 Создан: {created_at}", - "WATA_STATUS_SUCCESS": "✅ Платеж успешно завершен! Средства уже на балансе.", - "WATA_STATUS_PENDING": "⏳ Платеж еще не завершен. После оплаты вернитесь и проверьте статус.", - "WATA_STATUS_PAYMENT_URL": "🌐 Ссылка на оплату: {url}", - "WATA_STATUS_REMOTE_STATUS": "ℹ️ Последний статус транзакции: {status}", - "WATA_STATUS_LINK_OPENED": "Ожидает оплаты", - "WATA_STATUS_LINK_CLOSED": "Ссылка закрыта", - "WATA_STATUS_LINK_UNKNOWN": "Неизвестно", - "WATA_STATUS_TRANSACTION_PAID": "Оплачен", - "WATA_STATUS_TRANSACTION_DECLINED": "Отклонен", - "WATA_STATUS_TRANSACTION_PENDING": "В обработке", - "WATA_STATUS_TRANSACTION_UNKNOWN": "Неизвестно", - "WATA_STATUS_ERROR": "❌ Ошибка проверки статуса", "MY_TICKETS_BUTTON": "📋 Мои тикеты", "MY_TICKETS_TITLE": "📋 Ваши тикеты:", "NOTIFICATION_CLOSED": "Уведомление закрыто.", @@ -652,12 +629,9 @@ "PAL24_PAY_BUTTON": "🏦 Оплатить через PayPalych (СБП)", "PAL24_TOPUP_PROMPT": "🏦 Оплата через PayPalych (СБП)\n\nВведите сумму для пополнения от 100 до 1 000 000 ₽.\nОплата проходит через систему быстрых платежей PayPalych.", "PAYMENT_CARD_MULENPAY": "💳 Банковская карта (Mulen Pay)", - "PAYMENT_CARD_WATA": "🌐 Банковская карта (Wata Pay)", "PAYMENT_CARD_PAL24": "🏦 СБП (PayPalych)", "PAYMENT_METHOD_MULENPAY_DESCRIPTION": "через Mulen Pay", "PAYMENT_METHOD_MULENPAY_NAME": "💳 Банковская карта (Mulen Pay)", - "PAYMENT_METHOD_WATA_DESCRIPTION": "через Wata Pay", - "PAYMENT_METHOD_WATA_NAME": "🌐 Банковская карта (Wata Pay)", "PAYMENT_METHOD_PAL24_DESCRIPTION": "через систему быстрых платежей", "PAYMENT_METHOD_PAL24_NAME": "🏦 СБП (PayPalych)", "REPLY_TO_TICKET": "💬 Ответить", diff --git a/app/services/admin_notification_service.py b/app/services/admin_notification_service.py index 52310896..65a728ae 100644 --- a/app/services/admin_notification_service.py +++ b/app/services/admin_notification_service.py @@ -764,7 +764,6 @@ class AdminNotificationService: 'yookassa': '💳 YooKassa (карта)', 'tribute': '💎 Tribute (карта)', 'mulenpay': '💳 Mulen Pay (карта)', - 'wata': '🌐 Wata Pay (карта)', 'pal24': '🏦 PayPalych (СБП)', 'manual': '🛠️ Вручную (админ)', 'balance': '💰 С баланса' diff --git a/app/services/backup_service.py b/app/services/backup_service.py index d1297713..abfde7e5 100644 --- a/app/services/backup_service.py +++ b/app/services/backup_service.py @@ -24,7 +24,7 @@ from app.database.models import ( ServerSquad, SubscriptionServer, UserMessage, YooKassaPayment, CryptoBotPayment, WelcomeText, Base, PromoGroup, AdvertisingCampaign, AdvertisingCampaignRegistration, SupportAuditLog, Ticket, TicketMessage, - MulenPayPayment, Pal24Payment, WataPayment, DiscountOffer, WebApiToken, + MulenPayPayment, Pal24Payment, DiscountOffer, WebApiToken, server_squad_promo_groups ) @@ -82,7 +82,6 @@ class BackupService: CryptoBotPayment, MulenPayPayment, Pal24Payment, - WataPayment, PromoCodeUse, ReferralEarning, SentNotification, diff --git a/app/services/payment/__init__.py b/app/services/payment/__init__.py index 71719bfb..870c33af 100644 --- a/app/services/payment/__init__.py +++ b/app/services/payment/__init__.py @@ -11,7 +11,6 @@ from .tribute import TributePaymentMixin from .cryptobot import CryptoBotPaymentMixin from .mulenpay import MulenPayPaymentMixin from .pal24 import Pal24PaymentMixin -from .wata import WataPaymentMixin __all__ = [ "PaymentCommonMixin", @@ -21,5 +20,4 @@ __all__ = [ "CryptoBotPaymentMixin", "MulenPayPaymentMixin", "Pal24PaymentMixin", - "WataPaymentMixin", ] diff --git a/app/services/payment/wata.py b/app/services/payment/wata.py deleted file mode 100644 index 44c91e04..00000000 --- a/app/services/payment/wata.py +++ /dev/null @@ -1,422 +0,0 @@ -"""Mixin, инкапсулирующий работу с платежами Wata Pay.""" - -from __future__ import annotations - -import json -import logging -from datetime import datetime -from decimal import Decimal, InvalidOperation -from importlib import import_module -from typing import Any, Dict, Optional - -from dateutil import parser -from sqlalchemy.ext.asyncio import AsyncSession - -from app.config import settings -from app.database.models import PaymentMethod, TransactionType -from app.localization.texts import get_texts -from app.utils.user_utils import format_referrer_info - -logger = logging.getLogger(__name__) - - -class WataPaymentMixin: - """Mixin с созданием платежей, обработкой webhook и синхронизацией статусов Wata.""" - - async def create_wata_payment( - self, - db: AsyncSession, - *, - user_id: int, - amount_kopeks: int, - description: str, - language: Optional[str] = None, - ) -> Optional[Dict[str, Any]]: - service = getattr(self, "wata_service", None) - if not service or not service.is_configured: - logger.error("Wata сервис не инициализирован") - 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 - - order_id = f"wata_{user_id}_{datetime.utcnow().strftime('%Y%m%d%H%M%S%f')}" - - payload = { - "type": settings.WATA_LINK_TYPE or "OneTime", - "amount": self._format_amount(amount_kopeks), - "currency": settings.WATA_DEFAULT_CURRENCY or "RUB", - "description": description, - "orderId": order_id, - } - - if settings.WATA_ALLOW_ARBITRARY_AMOUNT: - payload["isArbitraryAmountAllowed"] = True - - if settings.WATA_SUCCESS_REDIRECT_URL: - payload["successRedirectUrl"] = settings.WATA_SUCCESS_REDIRECT_URL - if settings.WATA_FAIL_REDIRECT_URL: - payload["failRedirectUrl"] = settings.WATA_FAIL_REDIRECT_URL - - try: - response = await service.create_payment_link(**payload) - except Exception as error: # pragma: no cover - network failures - logger.error("Ошибка Wata API при создании ссылки: %s", error) - return None - - if not response: - logger.error("Пустой ответ при создании Wata ссылки") - return None - - payment_url = response.get("url") - link_id = response.get("id") - status = response.get("status", "Opened") - - if not payment_url: - logger.error("Wata не вернул ссылку на оплату: %s", response) - return None - - expiration_at = self._parse_datetime(response.get("expirationDateTime")) - - metadata = { - "raw_response": response, - "language": language or "ru", - } - - payment_module = import_module("app.services.payment_service") - payment = await payment_module.create_wata_payment( - db, - user_id=user_id, - amount_kopeks=amount_kopeks, - order_id=order_id, - description=description, - status=status, - currency=response.get("currency", settings.WATA_DEFAULT_CURRENCY or "RUB"), - payment_url=payment_url, - wata_link_id=link_id, - success_redirect_url=response.get("successRedirectUrl"), - fail_redirect_url=response.get("failRedirectUrl"), - expiration_at=expiration_at, - metadata=metadata, - ) - - logger.info( - "Создан Wata платеж %s для пользователя %s (%s₽)", - order_id, - user_id, - amount_kopeks / 100, - ) - - return { - "local_payment_id": payment.id, - "order_id": order_id, - "link_id": link_id, - "payment_url": payment_url, - "status": status, - "amount_kopeks": amount_kopeks, - } - - async def verify_wata_webhook_signature( - self, - raw_body: bytes, - signature: str, - ) -> bool: - service = getattr(self, "wata_service", None) - if not service or not service.is_configured: - logger.error("Wata сервис не инициализирован для проверки подписи") - return False - - try: - return await service.verify_signature(raw_body, signature) - except Exception as error: # pragma: no cover - безопасность - logger.error("Ошибка проверки подписи Wata: %s", error) - return False - - async def process_wata_webhook( - self, - db: AsyncSession, - payload: Dict[str, Any], - ) -> bool: - try: - payment_module = import_module("app.services.payment_service") - - order_id = payload.get("orderId") or payload.get("order_id") - link_id = payload.get("paymentLinkId") or payload.get("payment_link_id") - transaction_status = payload.get("transactionStatus") or payload.get("transaction_status") - transaction_id = payload.get("transactionId") or payload.get("transaction_id") - amount_value = payload.get("amount") - - payment = None - - if order_id: - payment = await payment_module.get_wata_payment_by_order_id(db, order_id) - if not payment and link_id: - payment = await payment_module.get_wata_payment_by_link_id(db, link_id) - - if not payment: - logger.error( - "Wata платеж не найден (order_id=%s, link_id=%s)", - order_id, - link_id, - ) - return False - - status = payload.get("status") or payment.status - await payment_module.update_wata_payment_status( - db, - payment=payment, - status=status, - transaction_status=transaction_status, - callback_payload=payload, - external_transaction_id=transaction_id, - ) - - payment = await payment_module.get_wata_payment_by_local_id(db, payment.id) - - if payment.is_paid: - logger.info("Wata платеж %s уже оплачен", payment.order_id) - return True - - if (transaction_status or "").lower() == "paid": - amount_kopeks = self._parse_amount_to_kopeks(amount_value) - if amount_kopeks is None: - amount_kopeks = payment.amount_kopeks - - return await self._finalize_wata_payment( - db, - payment, - amount_kopeks=amount_kopeks, - transaction_id=transaction_id or payment.order_id, - transaction_payload=payload, - ) - - return True - - except Exception as error: - logger.error("Ошибка обработки Wata webhook: %s", error, exc_info=True) - return False - - async def get_wata_payment_status( - self, - db: AsyncSession, - local_payment_id: int, - ) -> Optional[Dict[str, Any]]: - try: - payment_module = import_module("app.services.payment_service") - payment = await payment_module.get_wata_payment_by_local_id(db, local_payment_id) - if not payment: - return None - - service = getattr(self, "wata_service", None) - remote_link: Optional[Dict[str, Any]] = None - remote_transaction: Optional[Dict[str, Any]] = None - - if service and service.is_configured: - if payment.wata_link_id: - remote_link = await service.get_payment_link(payment.wata_link_id) - if remote_link: - await payment_module.update_wata_payment_status( - db, - payment=payment, - status=remote_link.get("status", payment.status), - payment_url=remote_link.get("url", payment.payment_url), - last_status_payload=remote_link, - ) - payment = await payment_module.get_wata_payment_by_local_id(db, local_payment_id) - - if payment.order_id: - try: - transactions_response = await service.search_transactions(order_id=payment.order_id) - except Exception as error: # pragma: no cover - сеть - logger.error("Ошибка запроса транзакций Wata: %s", error) - transactions_response = None - - if transactions_response: - items = transactions_response.get("items") - if isinstance(items, list): - for item in items: - if not isinstance(item, dict): - continue - remote_transaction = item - status = (item.get("status") or "").lower() - if status == "paid" and not payment.is_paid: - amount_kopeks = self._parse_amount_to_kopeks(item.get("amount")) - if amount_kopeks is None: - amount_kopeks = payment.amount_kopeks - await self._finalize_wata_payment( - db, - payment, - amount_kopeks=amount_kopeks, - transaction_id=item.get("id") or payment.order_id, - transaction_payload=item, - ) - payment = await payment_module.get_wata_payment_by_local_id( - db, local_payment_id - ) - break - - return { - "payment": payment, - "remote_link": remote_link, - "remote_transaction": remote_transaction, - } - - except Exception as error: - logger.error("Ошибка получения статуса Wata: %s", error, exc_info=True) - return None - - async def _finalize_wata_payment( - self, - db: AsyncSession, - payment: Any, - *, - amount_kopeks: int, - transaction_id: str, - transaction_payload: Optional[Dict[str, Any]] = None, - ) -> bool: - payment_module = import_module("app.services.payment_service") - - await payment_module.update_wata_payment_status( - db, - payment=payment, - status="Closed", - transaction_status="Paid", - is_paid=True, - paid_at=datetime.utcnow(), - callback_payload=transaction_payload, - external_transaction_id=transaction_id, - ) - - transaction = await payment_module.create_transaction( - db=db, - user_id=payment.user_id, - type=TransactionType.DEPOSIT, - amount_kopeks=amount_kopeks, - description=f"Пополнение через Wata Pay: {payment.description or payment.order_id}", - payment_method=PaymentMethod.WATA, - external_id=transaction_id, - metadata={"wata_payload": transaction_payload} if transaction_payload else None, - is_completed=True, - ) - - await payment_module.link_wata_payment_to_transaction( - db=db, - payment=payment, - transaction_id=transaction.id, - ) - - user = await payment_module.get_user_by_id(db, payment.user_id) - if not user: - logger.error("Пользователь %s не найден для Wata платежа", payment.user_id) - return False - - old_balance = user.balance_kopeks - was_first_topup = not user.has_made_first_topup - - user.balance_kopeks += amount_kopeks - user.updated_at = datetime.utcnow() - - if was_first_topup: - user.has_made_first_topup = True - - await db.commit() - await db.refresh(user) - - try: - from app.services.referral_service import process_referral_topup - - await process_referral_topup( - db, - user.id, - amount_kopeks, - getattr(self, "bot", None), - ) - except Exception as error: - logger.error("Ошибка обработки реферального пополнения Wata: %s", error) - - 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 "🔄 Пополнение" - - 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) and user.telegram_id: - try: - texts = get_texts(getattr(user, "language", settings.DEFAULT_LANGUAGE)) - payment_method_title = texts.t( - "PAYMENT_CARD_WATA", - "🌐 Банковская карта (Wata Pay)", - ) - await self._send_payment_success_notification( - user.telegram_id, - amount_kopeks, - user=user, - db=db, - payment_method_title=payment_method_title, - ) - except Exception as error: - logger.error("Ошибка отправки уведомления пользователю Wata: %s", error) - - logger.info( - "✅ Обработан Wata платеж %s для пользователя %s", - payment.order_id, - payment.user_id, - ) - return True - - def _format_amount(self, amount_kopeks: int) -> str: - return f"{Decimal(amount_kopeks) / Decimal(100):.2f}" - - def _parse_amount_to_kopeks(self, amount: Any) -> Optional[int]: - if amount is None: - return None - try: - if isinstance(amount, (int, float, Decimal)): - decimal_amount = Decimal(str(amount)) - elif isinstance(amount, str): - decimal_amount = Decimal(amount) - else: - decimal_amount = Decimal(json.dumps(amount)) - return int(decimal_amount * 100) - except (InvalidOperation, TypeError, ValueError): - return None - - def _parse_datetime(self, value: Any) -> Optional[datetime]: - if not value: - return None - try: - return parser.isoparse(value) - except (ValueError, TypeError, AttributeError): # pragma: no cover - формат - return None diff --git a/app/services/payment_service.py b/app/services/payment_service.py index 63bf1d0d..c105e986 100644 --- a/app/services/payment_service.py +++ b/app/services/payment_service.py @@ -14,13 +14,11 @@ from app.external.cryptobot import CryptoBotService from app.external.telegram_stars import TelegramStarsService from app.services.mulenpay_service import MulenPayService from app.services.pal24_service import Pal24Service -from app.services.wata_service import WataService from app.services.payment import ( CryptoBotPaymentMixin, MulenPayPaymentMixin, Pal24PaymentMixin, PaymentCommonMixin, - WataPaymentMixin, TelegramStarsMixin, TributePaymentMixin, YooKassaPaymentMixin, @@ -128,36 +126,6 @@ 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_local_id(*args, **kwargs): - wata_crud = import_module("app.database.crud.wata") - return await wata_crud.get_wata_payment_by_local_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 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 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) @@ -186,7 +154,6 @@ class PaymentService( CryptoBotPaymentMixin, MulenPayPaymentMixin, Pal24PaymentMixin, - WataPaymentMixin, ): """Основной интерфейс платежей, делегирующий работу специализированным mixin-ам.""" @@ -207,17 +174,13 @@ 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, Wata=%s)", + "MulenPay=%s, Pal24=%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), ) diff --git a/app/services/user_service.py b/app/services/user_service.py index b77d1a91..9dc4012f 100644 --- a/app/services/user_service.py +++ b/app/services/user_service.py @@ -21,7 +21,7 @@ from app.database.models import ( User, UserStatus, Subscription, Transaction, PromoCode, PromoCodeUse, ReferralEarning, SubscriptionServer, YooKassaPayment, BroadcastHistory, CryptoBotPayment, SubscriptionConversion, UserMessage, WelcomeText, - SentNotification, PromoGroup, MulenPayPayment, Pal24Payment, WataPayment, + SentNotification, PromoGroup, MulenPayPayment, Pal24Payment, AdvertisingCampaign, AdvertisingCampaignRegistration, PaymentMethod, TransactionType ) @@ -687,27 +687,6 @@ class UserService: except Exception as e: logger.error(f"❌ Ошибка удаления Pal24 платежей: {e}") - try: - wata_result = await db.execute( - select(WataPayment).where(WataPayment.user_id == user_id) - ) - wata_payments = wata_result.scalars().all() - - if wata_payments: - logger.info(f"🔄 Удаляем {len(wata_payments)} Wata платежей") - await db.execute( - update(WataPayment) - .where(WataPayment.user_id == user_id) - .values(transaction_id=None) - ) - await db.flush() - await db.execute( - delete(WataPayment).where(WataPayment.user_id == user_id) - ) - await db.flush() - except Exception as e: - logger.error(f"❌ Ошибка удаления Wata платежей: {e}") - try: transactions_result = await db.execute( select(Transaction).where(Transaction.user_id == user_id) diff --git a/app/services/wata_service.py b/app/services/wata_service.py deleted file mode 100644 index 61e5244d..00000000 --- a/app/services/wata_service.py +++ /dev/null @@ -1,150 +0,0 @@ -"""Интеграция с API Wata Pay.""" - -from __future__ import annotations - -import asyncio -import base64 -import logging -import time -from typing import Any, Dict, Optional - -import aiohttp -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import padding - -from app.config import settings - -logger = logging.getLogger(__name__) - - -class WataService: - """Обёртка над REST API Wata Pay.""" - - def __init__(self) -> None: - self.base_url = (settings.WATA_BASE_URL or "https://api.wata.pro/api/h2h").rstrip("/") - self.access_token = settings.WATA_ACCESS_TOKEN - self._public_key_cache: Optional[tuple[str, float]] = None - self._public_key_lock = asyncio.Lock() - - @property - def is_configured(self) -> bool: - return bool(settings.is_wata_enabled() and self.access_token) - - async def _request( - self, - method: str, - endpoint: str, - *, - json_data: Optional[Dict[str, Any]] = None, - params: Optional[Dict[str, Any]] = None, - ) -> Optional[Dict[str, Any]]: - if not self.is_configured: - logger.error("Wata service is not configured") - return None - - url = f"{self.base_url}{endpoint}" - headers = { - "Authorization": f"Bearer {self.access_token}", - "Content-Type": "application/json", - } - - timeout = aiohttp.ClientTimeout(total=settings.WATA_TIMEOUT_SECONDS or 60) - - try: - async with aiohttp.ClientSession(timeout=timeout) as session: - async with session.request( - method, - url, - headers=headers, - json=json_data, - params=params, - ) as response: - text = await response.text() - if not text: - data: Dict[str, Any] = {} - else: - try: - data = await response.json(content_type=None) - except Exception: - logger.error( - "Wata API вернул не-JSON ответ %s: %s", response.status, text - ) - return None - - if response.status >= 400: - logger.error( - "Wata API error %s %s: %s", response.status, endpoint, data - ) - return None - - return data - except aiohttp.ClientError as error: - logger.error("Wata API request error: %s", error) - return None - except Exception as error: # pragma: no cover - непредвиденные ошибки сети - logger.error("Unexpected Wata error: %s", error, exc_info=True) - return None - - async def create_payment_link(self, **payload: Any) -> Optional[Dict[str, Any]]: - return await self._request("POST", "/links", json_data=payload) - - async def get_payment_link(self, link_id: str) -> Optional[Dict[str, Any]]: - return await self._request("GET", f"/links/{link_id}") - - async def search_transactions( - self, - *, - order_id: Optional[str] = None, - ) -> Optional[Dict[str, Any]]: - params: Dict[str, Any] = {} - if order_id: - params["orderId"] = order_id - return await self._request("GET", "/transactions/", params=params or None) - - async def get_public_key(self, *, force: bool = False) -> Optional[str]: - if not force and self._public_key_cache: - value, expires_at = self._public_key_cache - if time.time() < expires_at: - return value - - async with self._public_key_lock: - if not force and self._public_key_cache: - value, expires_at = self._public_key_cache - if time.time() < expires_at: - return value - - response = await self._request("GET", "/public-key") - if not response: - return None - - value = response.get("value") - if not isinstance(value, str): - logger.error("Некорректный публичный ключ Wata: %s", response) - return None - - self._public_key_cache = (value, time.time() + 3600) - return value - - async def verify_signature(self, raw_body: bytes, signature: str) -> bool: - if not signature: - logger.error("Отсутствует подпись Wata webhook") - return False - - public_key_pem = await self.get_public_key() - if not public_key_pem: - logger.error("Не удалось получить публичный ключ Wata") - return False - - try: - public_key = serialization.load_pem_public_key(public_key_pem.encode()) - signature_bytes = base64.b64decode(signature) - public_key.verify( - signature_bytes, - raw_body, - padding.PKCS1v15(), - hashes.SHA512(), - ) - return True - except Exception as error: # pragma: no cover - безопасность - logger.error("Ошибка проверки подписи Wata: %s", error) - return False diff --git a/app/utils/payment_utils.py b/app/utils/payment_utils.py index a0fe402e..913b18d2 100644 --- a/app/utils/payment_utils.py +++ b/app/utils/payment_utils.py @@ -54,15 +54,6 @@ 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 Pay", - "callback": "topup_wata" - }) - if settings.is_pal24_enabled(): methods.append({ "id": "pal24", @@ -150,8 +141,6 @@ 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": @@ -170,7 +159,6 @@ 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 @@ -189,8 +177,6 @@ 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(): diff --git a/tests/handlers/test_balance_wata.py b/tests/handlers/test_balance_wata.py deleted file mode 100644 index 8abd95ea..00000000 --- a/tests/handlers/test_balance_wata.py +++ /dev/null @@ -1,120 +0,0 @@ -from __future__ import annotations - -import sys -from datetime import datetime -from pathlib import Path -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)) - -from app.handlers.balance import check_wata_payment_status # noqa: E402 - - -@pytest.fixture -def anyio_backend() -> str: - return "asyncio" - - -class DummyMessage: - def __init__(self) -> None: - self.calls: list[tuple[str, Dict[str, Any]]] = [] - - async def answer(self, text: str, **kwargs: Any) -> None: - self.calls.append((text, kwargs)) - - -class DummyCallback: - def __init__(self, data: str) -> None: - self.data = data - self.message = DummyMessage() - self.bot = object() - self.answers: list[tuple[str, bool]] = [] - - async def answer(self, text: str = "", show_alert: bool = False) -> None: - self.answers.append((text, show_alert)) - - -class DummyPayment: - def __init__(self) -> None: - self.order_id = "WATA-TEST" - self.amount_kopeks = 2_500 - self.status = "Opened" - self.transaction_status: Optional[str] = None - self.created_at = datetime(2024, 1, 5, 12, 30) - self.is_paid = False - self.payment_url = "https://pay.example" - self.user = type("U", (), {"language": "en"})() - - -@pytest.mark.anyio("asyncio") -async def test_check_wata_payment_status_success(monkeypatch: pytest.MonkeyPatch) -> None: - payment = DummyPayment() - remote_transaction = {"status": "Paid"} - - class FakePaymentService: - def __init__(self, bot: Any) -> None: - self.bot = bot - - async def get_wata_payment_status( - self, - db: Any, - local_payment_id: int, - ) -> Dict[str, Any] | None: - assert local_payment_id == 5 - payment.transaction_status = "Paid" - payment.is_paid = True - return { - "payment": payment, - "remote_transaction": remote_transaction, - } - - monkeypatch.setattr( - "app.handlers.balance.PaymentService", - FakePaymentService, - raising=False, - ) - - callback = DummyCallback("check_wata_5") - - await check_wata_payment_status(callback, db=None) - - assert callback.answers[0] == ("", False) - assert callback.message.calls, "expected message to be sent" - message_text, kwargs = callback.message.calls[0] - assert "Wata Pay payment status" in message_text - assert "Order ID: WATA-TEST" in message_text - assert "https://pay.example" not in message_text - assert kwargs["disable_web_page_preview"] is True - - -@pytest.mark.anyio("asyncio") -async def test_check_wata_payment_status_not_found(monkeypatch: pytest.MonkeyPatch) -> None: - class FakePaymentService: - def __init__(self, bot: Any) -> None: - self.bot = bot - - async def get_wata_payment_status( - self, - db: Any, - local_payment_id: int, - ) -> Dict[str, Any] | None: - return None - - monkeypatch.setattr( - "app.handlers.balance.PaymentService", - FakePaymentService, - raising=False, - ) - - callback = DummyCallback("check_wata_77") - - await check_wata_payment_status(callback, db=None) - - assert callback.answers - not_found_text, alert = callback.answers[0] - assert alert is True - assert "Платеж не найден" in not_found_text diff --git a/tests/services/test_payment_service_modularity.py b/tests/services/test_payment_service_modularity.py index b2f7546f..6c0ead84 100644 --- a/tests/services/test_payment_service_modularity.py +++ b/tests/services/test_payment_service_modularity.py @@ -14,7 +14,6 @@ from app.services.payment import ( # noqa: E402 MulenPayPaymentMixin, Pal24PaymentMixin, PaymentCommonMixin, - WataPaymentMixin, TelegramStarsMixin, TributePaymentMixin, YooKassaPaymentMixin, @@ -32,7 +31,6 @@ 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-классы" @@ -47,7 +45,6 @@ def test_payment_service_mro_contains_all_mixins() -> None: "create_tribute_payment", "create_cryptobot_payment", "create_mulenpay_payment", - "create_wata_payment", "create_pal24_payment", ], ) diff --git a/tests/services/test_payment_service_mulenpay.py b/tests/services/test_payment_service_mulenpay.py index 9bda78dd..68eec241 100644 --- a/tests/services/test_payment_service_mulenpay.py +++ b/tests/services/test_payment_service_mulenpay.py @@ -50,7 +50,6 @@ def _make_service(stub: Optional[StubMulenPayService]) -> PaymentService: service.yookassa_service = None service.stars_service = None service.cryptobot_service = None - service.wata_service = None return service diff --git a/tests/services/test_payment_service_wata.py b/tests/services/test_payment_service_wata.py deleted file mode 100644 index bc05267d..00000000 --- a/tests/services/test_payment_service_wata.py +++ /dev/null @@ -1,288 +0,0 @@ -from __future__ import annotations - -import sys -from pathlib import Path -from datetime import datetime -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 - return None - - -class DummyPayment: - def __init__(self, payment_id: int = 101) -> None: - self.id = payment_id - - -class DummyWataPayment: - def __init__(self) -> None: - self.id = 11 - self.user_id = 77 - self.amount_kopeks = 50000 - self.order_id = "wata_77_test" - self.status = "Opened" - self.transaction_status = None - self.is_paid = False - self.wata_link_id = "link" - self.payment_url = "https://pay" - self.created_at = datetime(2024, 1, 1, 12, 0, 0) - self.user = type("U", (), {"language": "ru", "telegram_id": 123})() - - -class StubWataService: - def __init__(self, response: Optional[Dict[str, Any]]) -> None: - self.response = response - self.calls: list[Dict[str, Any]] = [] - self.is_configured = True - - async def create_payment_link(self, **payload: Any) -> Optional[Dict[str, Any]]: - self.calls.append(payload) - return self.response - - -def _make_service(stub: Optional[StubWataService]) -> PaymentService: - service = PaymentService.__new__(PaymentService) # type: ignore[call-arg] - service.bot = None - service.yookassa_service = None - service.stars_service = None - service.cryptobot_service = None - service.mulenpay_service = None - service.pal24_service = None - service.wata_service = stub - return service - - -@pytest.mark.anyio("asyncio") -async def test_create_wata_payment_success(monkeypatch: pytest.MonkeyPatch) -> None: - stub = StubWataService({"id": "link", "url": "https://pay"}) - service = _make_service(stub) - db = DummySession() - - captured_kwargs: Dict[str, Any] = {} - - async def fake_create_wata_payment(*args: Any, **kwargs: Any) -> DummyPayment: - captured_kwargs.update(kwargs) - return DummyPayment(payment_id=555) - - monkeypatch.setattr( - payment_service_module, - "create_wata_payment", - fake_create_wata_payment, - raising=False, - ) - - monkeypatch.setattr(settings, "WATA_MIN_AMOUNT_KOPEKS", 1_000, raising=False) - monkeypatch.setattr(settings, "WATA_MAX_AMOUNT_KOPEKS", 1_000_000_00, raising=False) - monkeypatch.setattr(settings, "WATA_DEFAULT_CURRENCY", "RUB", raising=False) - - result = await service.create_wata_payment( - db=db, - user_id=7, - amount_kopeks=25000, - description="Пополнение", - language="ru", - ) - - assert result is not None - assert result["local_payment_id"] == 555 - assert result["payment_url"] == "https://pay" - assert stub.calls and stub.calls[0]["amount"] == "250.00" - assert captured_kwargs["order_id"].startswith("wata_7_") - - -@pytest.mark.anyio("asyncio") -async def test_create_wata_payment_amount_limits(monkeypatch: pytest.MonkeyPatch) -> None: - stub = StubWataService({"id": "link", "url": "https://pay"}) - service = _make_service(stub) - db = DummySession() - - monkeypatch.setattr(settings, "WATA_MIN_AMOUNT_KOPEKS", 5000, raising=False) - monkeypatch.setattr(settings, "WATA_MAX_AMOUNT_KOPEKS", 10_000, raising=False) - - low_result = await service.create_wata_payment( - db=db, - user_id=1, - amount_kopeks=1000, - description="Пополнение", - ) - assert low_result is None - - high_result = await service.create_wata_payment( - db=db, - user_id=1, - amount_kopeks=20_000, - description="Пополнение", - ) - assert high_result is None - assert not stub.calls - - -@pytest.mark.anyio("asyncio") -async def test_create_wata_payment_without_service() -> None: - service = _make_service(None) - db = DummySession() - - result = await service.create_wata_payment( - db=db, - user_id=1, - amount_kopeks=10_000, - description="Пополнение", - ) - - assert result is None - - -@pytest.mark.anyio("asyncio") -async def test_process_wata_webhook_paid(monkeypatch: pytest.MonkeyPatch) -> None: - service = _make_service(None) - db = DummySession() - payment = DummyWataPayment() - - async def fake_get_by_order_id(db_session: Any, order_id: str) -> DummyWataPayment | None: - assert order_id == payment.order_id - return payment - - async def fake_get_by_link_id(db_session: Any, link_id: str) -> DummyWataPayment | None: - return None - - async def fake_get_by_local_id(db_session: Any, payment_id: int) -> DummyWataPayment | None: - assert payment_id == payment.id - return payment - - async def fake_update_status( - db_session: Any, - *, - payment: DummyWataPayment, - status: str | None = None, - transaction_status: str | None = None, - is_paid: bool | None = None, - paid_at: datetime | None = None, - callback_payload: dict | None = None, - external_transaction_id: str | None = None, - payment_url: str | None = None, - last_status_payload: dict | None = None, - ) -> DummyWataPayment: - if status is not None: - payment.status = status - if transaction_status is not None: - payment.transaction_status = transaction_status - if is_paid is not None: - payment.is_paid = is_paid - if payment_url is not None: - payment.payment_url = payment_url - if callback_payload is not None: - payment.callback_payload = callback_payload - if external_transaction_id is not None: - payment.external_transaction_id = external_transaction_id - if last_status_payload is not None: - payment.last_status_payload = last_status_payload - return payment - - finalize_calls: Dict[str, Any] = {} - - async def fake_finalize( - self: PaymentService, - db_session: Any, - payment: DummyWataPayment, - *, - amount_kopeks: int, - transaction_id: str, - transaction_payload: Optional[Dict[str, Any]] = None, - ) -> bool: - finalize_calls["amount"] = amount_kopeks - finalize_calls["transaction_id"] = transaction_id - payment.is_paid = True - return True - - monkeypatch.setattr( - payment_service_module, - "get_wata_payment_by_order_id", - fake_get_by_order_id, - raising=False, - ) - monkeypatch.setattr( - payment_service_module, - "get_wata_payment_by_link_id", - fake_get_by_link_id, - raising=False, - ) - monkeypatch.setattr( - payment_service_module, - "get_wata_payment_by_local_id", - fake_get_by_local_id, - raising=False, - ) - monkeypatch.setattr( - payment_service_module, - "update_wata_payment_status", - fake_update_status, - raising=False, - ) - monkeypatch.setattr( - PaymentService, - "_finalize_wata_payment", - fake_finalize, - raising=False, - ) - - payload = { - "orderId": payment.order_id, - "transactionStatus": "Paid", - "transactionId": "tx-123", - "amount": "500.00", - } - - processed = await service.process_wata_webhook(db, payload) - - assert processed is True - assert finalize_calls["amount"] == 50_000 - assert finalize_calls["transaction_id"] == "tx-123" - assert payment.is_paid is True - - -@pytest.mark.anyio("asyncio") -async def test_process_wata_webhook_missing_payment(monkeypatch: pytest.MonkeyPatch) -> None: - service = _make_service(None) - db = DummySession() - - async def fake_get_by_order_id(db_session: Any, order_id: str) -> None: - return None - - async def fake_get_by_link_id(db_session: Any, link_id: str) -> None: - return None - - monkeypatch.setattr( - payment_service_module, - "get_wata_payment_by_order_id", - fake_get_by_order_id, - raising=False, - ) - monkeypatch.setattr( - payment_service_module, - "get_wata_payment_by_link_id", - fake_get_by_link_id, - raising=False, - ) - - payload = {"orderId": "missing", "transactionStatus": "Paid"} - - processed = await service.process_wata_webhook(db, payload) - - assert processed is False diff --git a/tests/services/test_wata_service_adapter.py b/tests/services/test_wata_service_adapter.py deleted file mode 100644 index 933c4d01..00000000 --- a/tests/services/test_wata_service_adapter.py +++ /dev/null @@ -1,107 +0,0 @@ -from __future__ import annotations - -import base64 -import sys -from pathlib import Path -from typing import Any, Dict, Optional - -import pytest -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import padding, rsa - -ROOT_DIR = Path(__file__).resolve().parents[2] -if str(ROOT_DIR) not in sys.path: - sys.path.insert(0, str(ROOT_DIR)) - -from app.config import settings # noqa: E402 -from app.services.wata_service import WataService # noqa: E402 - - -@pytest.fixture -def anyio_backend() -> str: - return "asyncio" - - -def _enable_service(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(type(settings), "is_wata_enabled", lambda self: True, raising=False) - monkeypatch.setattr(settings, "WATA_ACCESS_TOKEN", "token", raising=False) - monkeypatch.setattr(settings, "WATA_BASE_URL", "https://wata.test", raising=False) - - -def test_is_configured(monkeypatch: pytest.MonkeyPatch) -> None: - service = WataService() - assert not service.is_configured - - _enable_service(monkeypatch) - service = WataService() - assert service.is_configured - - -@pytest.mark.anyio("asyncio") -async def test_create_payment_link(monkeypatch: pytest.MonkeyPatch) -> None: - _enable_service(monkeypatch) - - captured: Dict[str, Any] = {} - - async def fake_request(method: str, endpoint: str, **kwargs: Any) -> Dict[str, Any]: - captured.update({"method": method, "endpoint": endpoint, **kwargs}) - return {"id": "link", "url": "https://pay"} - - service = WataService() - monkeypatch.setattr(service, "_request", fake_request, raising=False) - - response = await service.create_payment_link(amount="100.00", orderId="test") - - assert response == {"id": "link", "url": "https://pay"} - assert captured["method"] == "POST" - assert captured["endpoint"] == "/links" - assert captured["json_data"]["orderId"] == "test" - - -@pytest.mark.anyio("asyncio") -async def test_get_public_key_caching(monkeypatch: pytest.MonkeyPatch) -> None: - _enable_service(monkeypatch) - service = WataService() - - calls: list[Dict[str, Any]] = [] - - async def fake_request(method: str, endpoint: str, **kwargs: Any) -> Dict[str, Any]: - calls.append({"method": method, "endpoint": endpoint}) - return {"value": "-----BEGIN PUBLIC KEY-----\nAAAA\n-----END PUBLIC KEY-----"} - - monkeypatch.setattr(service, "_request", fake_request, raising=False) - - key1 = await service.get_public_key() - key2 = await service.get_public_key() - - assert key1 == key2 - assert len(calls) == 1 - - -@pytest.mark.anyio("asyncio") -async def test_verify_signature(monkeypatch: pytest.MonkeyPatch) -> None: - _enable_service(monkeypatch) - service = WataService() - - private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) - public_key = private_key.public_key().public_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PublicFormat.SubjectPublicKeyInfo, - ).decode() - - async def fake_get_public_key(*args: Any, **kwargs: Any) -> Optional[str]: - return public_key - - monkeypatch.setattr(service, "get_public_key", fake_get_public_key, raising=False) - - payload = b"{\"event\":\"test\"}" - signature = private_key.sign( - payload, - padding.PKCS1v15(), - hashes.SHA512(), - ) - - signature_b64 = base64.b64encode(signature).decode() - - assert await service.verify_signature(payload, signature_b64) -