diff --git a/.env.example b/.env.example index 7d8db259..930a818e 100644 --- a/.env.example +++ b/.env.example @@ -451,23 +451,6 @@ PLATEGA_WEBHOOK_PATH=/platega-webhook PLATEGA_WEBHOOK_HOST=0.0.0.0 PLATEGA_WEBHOOK_PORT=8086 -# ===== FREEKASSA ===== -FREEKASSA_ENABLED=false -FREEKASSA_SHOP_ID= -FREEKASSA_API_KEY= -# Секретное слово 1 (для формы оплаты) -FREEKASSA_SECRET_WORD_1= -# Секретное слово 2 (для webhook) -FREEKASSA_SECRET_WORD_2= -FREEKASSA_DISPLAY_NAME=Freekassa -FREEKASSA_CURRENCY=RUB -FREEKASSA_MIN_AMOUNT_KOPEKS=10000 -FREEKASSA_MAX_AMOUNT_KOPEKS=100000000 -FREEKASSA_PAYMENT_TIMEOUT_SECONDS=3600 -FREEKASSA_WEBHOOK_PATH=/freekassa-webhook -FREEKASSA_WEBHOOK_HOST=0.0.0.0 -FREEKASSA_WEBHOOK_PORT=8088 - # ===== ИНТЕРФЕЙС И UX ===== # Включить логотип для всех сообщений (true - с изображением, false - только текст) diff --git a/app/config.py b/app/config.py index 1f0b8d0f..3361c0df 100644 --- a/app/config.py +++ b/app/config.py @@ -399,21 +399,6 @@ class Settings(BaseSettings): CLOUDPAYMENTS_REQUIRE_EMAIL: bool = False CLOUDPAYMENTS_TEST_MODE: bool = False - # Freekassa - FREEKASSA_ENABLED: bool = False - FREEKASSA_SHOP_ID: Optional[int] = None - FREEKASSA_API_KEY: Optional[str] = None - FREEKASSA_SECRET_WORD_1: Optional[str] = None # Для формы оплаты - FREEKASSA_SECRET_WORD_2: Optional[str] = None # Для webhook - FREEKASSA_DISPLAY_NAME: str = "Freekassa" - FREEKASSA_CURRENCY: str = "RUB" - FREEKASSA_MIN_AMOUNT_KOPEKS: int = 10000 # 100 руб - FREEKASSA_MAX_AMOUNT_KOPEKS: int = 100000000 # 1 000 000 руб - FREEKASSA_PAYMENT_TIMEOUT_SECONDS: int = 3600 - FREEKASSA_WEBHOOK_PATH: str = "/freekassa-webhook" - FREEKASSA_WEBHOOK_HOST: str = "0.0.0.0" - FREEKASSA_WEBHOOK_PORT: int = 8088 - MAIN_MENU_MODE: str = "default" CONNECT_BUTTON_MODE: str = "guide" MINIAPP_CUSTOM_URL: str = "" @@ -1429,22 +1414,6 @@ class Settings(BaseSettings): and self.CLOUDPAYMENTS_API_SECRET is not None ) - def is_freekassa_enabled(self) -> bool: - return ( - self.FREEKASSA_ENABLED - and self.FREEKASSA_SHOP_ID is not None - and self.FREEKASSA_API_KEY is not None - and self.FREEKASSA_SECRET_WORD_1 is not None - and self.FREEKASSA_SECRET_WORD_2 is not None - ) - - def get_freekassa_display_name(self) -> str: - name = (self.FREEKASSA_DISPLAY_NAME or "").strip() - return name if name else "Freekassa" - - def get_freekassa_display_name_html(self) -> str: - return html.escape(self.get_freekassa_display_name()) - def is_payment_verification_auto_check_enabled(self) -> bool: return self.PAYMENT_VERIFICATION_AUTO_CHECK_ENABLED diff --git a/app/database/crud/freekassa.py b/app/database/crud/freekassa.py deleted file mode 100644 index 0236a317..00000000 --- a/app/database/crud/freekassa.py +++ /dev/null @@ -1,159 +0,0 @@ -"""CRUD операции для платежей Freekassa.""" - -import json -import logging -from datetime import datetime -from typing import Optional, List - -from sqlalchemy import select -from sqlalchemy.ext.asyncio import AsyncSession - -from app.database.models import FreekassaPayment - -logger = logging.getLogger(__name__) - - -async def create_freekassa_payment( - db: AsyncSession, - *, - user_id: int, - order_id: str, - amount_kopeks: int, - currency: str = "RUB", - description: Optional[str] = None, - payment_url: Optional[str] = None, - expires_at: Optional[datetime] = None, - metadata_json: Optional[str] = None, -) -> FreekassaPayment: - """Создает запись о платеже Freekassa.""" - payment = FreekassaPayment( - user_id=user_id, - order_id=order_id, - amount_kopeks=amount_kopeks, - currency=currency, - description=description, - payment_url=payment_url, - expires_at=expires_at, - metadata_json=json.loads(metadata_json) if metadata_json else None, - status="pending", - is_paid=False, - ) - db.add(payment) - await db.commit() - await db.refresh(payment) - logger.info(f"Создан платеж Freekassa: order_id={order_id}, user_id={user_id}") - return payment - - -async def get_freekassa_payment_by_order_id( - db: AsyncSession, order_id: str -) -> Optional[FreekassaPayment]: - """Получает платеж по order_id.""" - result = await db.execute( - select(FreekassaPayment).where(FreekassaPayment.order_id == order_id) - ) - return result.scalar_one_or_none() - - -async def get_freekassa_payment_by_fk_order_id( - db: AsyncSession, freekassa_order_id: str -) -> Optional[FreekassaPayment]: - """Получает платеж по ID от Freekassa (intid).""" - result = await db.execute( - select(FreekassaPayment).where( - FreekassaPayment.freekassa_order_id == freekassa_order_id - ) - ) - return result.scalar_one_or_none() - - -async def get_freekassa_payment_by_id( - db: AsyncSession, payment_id: int -) -> Optional[FreekassaPayment]: - """Получает платеж по ID.""" - result = await db.execute( - select(FreekassaPayment).where(FreekassaPayment.id == payment_id) - ) - return result.scalar_one_or_none() - - -async def update_freekassa_payment_status( - db: AsyncSession, - payment: FreekassaPayment, - *, - status: str, - is_paid: bool = False, - freekassa_order_id: Optional[str] = None, - payment_system_id: Optional[int] = None, - callback_payload: Optional[dict] = None, - transaction_id: Optional[int] = None, -) -> FreekassaPayment: - """Обновляет статус платежа.""" - payment.status = status - payment.is_paid = is_paid - payment.updated_at = datetime.utcnow() - - if is_paid: - payment.paid_at = datetime.utcnow() - if freekassa_order_id: - payment.freekassa_order_id = freekassa_order_id - if payment_system_id is not None: - payment.payment_system_id = payment_system_id - if callback_payload: - payment.callback_payload = callback_payload - if transaction_id: - payment.transaction_id = transaction_id - - await db.commit() - await db.refresh(payment) - logger.info( - f"Обновлен статус платежа Freekassa: order_id={payment.order_id}, " - f"status={status}, is_paid={is_paid}" - ) - return payment - - -async def get_pending_freekassa_payments( - db: AsyncSession, user_id: int -) -> List[FreekassaPayment]: - """Получает незавершенные платежи пользователя.""" - result = await db.execute( - select(FreekassaPayment).where( - FreekassaPayment.user_id == user_id, - FreekassaPayment.status == "pending", - FreekassaPayment.is_paid == False, - ) - ) - return list(result.scalars().all()) - - -async def get_user_freekassa_payments( - db: AsyncSession, - user_id: int, - limit: int = 10, - offset: int = 0, -) -> List[FreekassaPayment]: - """Получает платежи пользователя с пагинацией.""" - result = await db.execute( - select(FreekassaPayment) - .where(FreekassaPayment.user_id == user_id) - .order_by(FreekassaPayment.created_at.desc()) - .limit(limit) - .offset(offset) - ) - return list(result.scalars().all()) - - -async def get_expired_pending_payments( - db: AsyncSession, -) -> List[FreekassaPayment]: - """Получает просроченные платежи в статусе pending.""" - now = datetime.utcnow() - result = await db.execute( - select(FreekassaPayment).where( - FreekassaPayment.status == "pending", - FreekassaPayment.is_paid == False, - FreekassaPayment.expires_at < now, - ) - ) - return list(result.scalars().all()) diff --git a/app/database/models.py b/app/database/models.py index 2cfbf892..504881c7 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -87,7 +87,6 @@ class PaymentMethod(Enum): WATA = "wata" PLATEGA = "platega" CLOUDPAYMENTS = "cloudpayments" - FREEKASSA = "freekassa" MANUAL = "manual" @@ -546,73 +545,6 @@ class CloudPaymentsPayment(Base): ) -class FreekassaPayment(Base): - __tablename__ = "freekassa_payments" - - id = Column(Integer, primary_key=True, index=True) - user_id = Column(Integer, ForeignKey("users.id"), nullable=False) - - # Идентификаторы - order_id = Column(String(64), unique=True, nullable=False, index=True) # Наш ID заказа - freekassa_order_id = Column(String(64), unique=True, nullable=True, index=True) # intid от Freekassa - - # Суммы - 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="pending") # pending, success, failed, expired - is_paid = Column(Boolean, default=False) - - # Данные платежа - payment_url = Column(Text, nullable=True) - payment_system_id = Column(Integer, nullable=True) # ID платежной системы FK - - # Метаданные - metadata_json = Column(JSON, nullable=True) - callback_payload = Column(JSON, nullable=True) - - # Временные метки - paid_at = Column(DateTime, nullable=True) - expires_at = Column(DateTime, nullable=True) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) - - # Связь с транзакцией - transaction_id = Column(Integer, ForeignKey("transactions.id"), nullable=True) - - # Relationships - user = relationship("User", backref="freekassa_payments") - transaction = relationship("Transaction", backref="freekassa_payment") - - @property - def amount_rubles(self) -> float: - return self.amount_kopeks / 100 - - @property - def is_pending(self) -> bool: - return self.status == "pending" - - @property - def is_success(self) -> bool: - return self.status == "success" and self.is_paid - - @property - def is_failed(self) -> bool: - return self.status in ["failed", "expired"] - - def __repr__(self) -> str: # pragma: no cover - debug helper - return ( - "".format( - self.id, - self.order_id, - self.amount_rubles, - self.status, - ) - ) - - class PromoGroup(Base): __tablename__ = "promo_groups" diff --git a/app/database/universal_migration.py b/app/database/universal_migration.py index cee17cfa..479a0c46 100644 --- a/app/database/universal_migration.py +++ b/app/database/universal_migration.py @@ -1289,118 +1289,6 @@ async def ensure_wata_payment_schema() -> bool: return False -async def create_freekassa_payments_table(): - """Создаёт таблицу freekassa_payments для платежей через Freekassa.""" - table_exists = await check_table_exists('freekassa_payments') - if table_exists: - logger.info("Таблица freekassa_payments уже существует") - return True - - try: - async with engine.begin() as conn: - db_type = await get_database_type() - - if db_type == 'sqlite': - create_sql = """ - CREATE TABLE freekassa_payments ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - order_id VARCHAR(64) NOT NULL UNIQUE, - freekassa_order_id VARCHAR(64) NULL UNIQUE, - amount_kopeks INTEGER NOT NULL, - currency VARCHAR(10) NOT NULL DEFAULT 'RUB', - description TEXT NULL, - status VARCHAR(32) NOT NULL DEFAULT 'pending', - is_paid BOOLEAN NOT NULL DEFAULT 0, - payment_url TEXT NULL, - payment_system_id INTEGER NULL, - metadata_json JSON NULL, - callback_payload JSON NULL, - paid_at DATETIME NULL, - expires_at DATETIME NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - transaction_id INTEGER NULL, - FOREIGN KEY (user_id) REFERENCES users(id), - FOREIGN KEY (transaction_id) REFERENCES transactions(id) - ); - - CREATE INDEX idx_freekassa_user_id ON freekassa_payments(user_id); - CREATE UNIQUE INDEX idx_freekassa_order_id ON freekassa_payments(order_id); - CREATE UNIQUE INDEX idx_freekassa_fk_order_id ON freekassa_payments(freekassa_order_id); - """ - - elif db_type == 'postgresql': - create_sql = """ - CREATE TABLE freekassa_payments ( - id SERIAL PRIMARY KEY, - user_id INTEGER NOT NULL REFERENCES users(id), - order_id VARCHAR(64) NOT NULL UNIQUE, - freekassa_order_id VARCHAR(64) NULL UNIQUE, - amount_kopeks INTEGER NOT NULL, - currency VARCHAR(10) NOT NULL DEFAULT 'RUB', - description TEXT NULL, - status VARCHAR(32) NOT NULL DEFAULT 'pending', - is_paid BOOLEAN NOT NULL DEFAULT FALSE, - payment_url TEXT NULL, - payment_system_id INTEGER NULL, - metadata_json JSON NULL, - callback_payload JSON NULL, - paid_at TIMESTAMP NULL, - expires_at TIMESTAMP NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - transaction_id INTEGER NULL REFERENCES transactions(id) - ); - - CREATE INDEX idx_freekassa_user_id ON freekassa_payments(user_id); - CREATE UNIQUE INDEX idx_freekassa_order_id ON freekassa_payments(order_id); - CREATE UNIQUE INDEX idx_freekassa_fk_order_id ON freekassa_payments(freekassa_order_id); - """ - - elif db_type == 'mysql': - create_sql = """ - CREATE TABLE freekassa_payments ( - id INT AUTO_INCREMENT PRIMARY KEY, - user_id INT NOT NULL, - order_id VARCHAR(64) NOT NULL UNIQUE, - freekassa_order_id VARCHAR(64) NULL UNIQUE, - amount_kopeks INT NOT NULL, - currency VARCHAR(10) NOT NULL DEFAULT 'RUB', - description TEXT NULL, - status VARCHAR(32) NOT NULL DEFAULT 'pending', - is_paid BOOLEAN NOT NULL DEFAULT 0, - payment_url TEXT NULL, - payment_system_id INT NULL, - metadata_json JSON NULL, - callback_payload JSON NULL, - paid_at DATETIME NULL, - expires_at DATETIME NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - transaction_id INT NULL, - FOREIGN KEY (user_id) REFERENCES users(id), - FOREIGN KEY (transaction_id) REFERENCES transactions(id) - ); - - CREATE INDEX idx_freekassa_user_id ON freekassa_payments(user_id); - CREATE UNIQUE INDEX idx_freekassa_order_id ON freekassa_payments(order_id); - CREATE UNIQUE INDEX idx_freekassa_fk_order_id ON freekassa_payments(freekassa_order_id); - """ - - else: - logger.error(f"Неподдерживаемый тип БД для таблицы freekassa_payments: {db_type}") - return False - - await conn.execute(text(create_sql)) - logger.info("Таблица freekassa_payments успешно создана") - return True - - except Exception as e: - logger.error(f"Ошибка создания таблицы freekassa_payments: {e}") - return False - - async def create_discount_offers_table(): table_exists = await check_table_exists('discount_offers') if table_exists: @@ -5192,13 +5080,6 @@ async def run_universal_migration(): else: logger.warning("⚠️ Не удалось обновить схему Wata payments") - logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ FREEKASSA ===") - freekassa_created = await create_freekassa_payments_table() - if freekassa_created: - logger.info("✅ Таблица Freekassa payments готова") - else: - logger.warning("⚠️ Проблемы с таблицей Freekassa 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 0a57abb7..ec366e6e 100644 --- a/app/external/webhook_server.py +++ b/app/external/webhook_server.py @@ -36,10 +36,7 @@ class WebhookServer: if settings.is_cryptobot_enabled(): self.app.router.add_post(settings.CRYPTOBOT_WEBHOOK_PATH, self._cryptobot_webhook_handler) - - if settings.is_freekassa_enabled(): - self.app.router.add_post(settings.FREEKASSA_WEBHOOK_PATH, self._freekassa_webhook_handler) - + self.app.router.add_get('/health', self._health_check) self.app.router.add_options(settings.TRIBUTE_WEBHOOK_PATH, self._options_handler) @@ -47,9 +44,7 @@ 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_freekassa_enabled(): - self.app.router.add_options(settings.FREEKASSA_WEBHOOK_PATH, self._options_handler) - + logger.info(f"Webhook сервер настроен:") logger.info(f" - Tribute webhook: POST {settings.TRIBUTE_WEBHOOK_PATH}") if settings.is_mulenpay_enabled(): @@ -61,8 +56,6 @@ class WebhookServer: ) if settings.is_cryptobot_enabled(): logger.info(f" - CryptoBot webhook: POST {settings.CRYPTOBOT_WEBHOOK_PATH}") - if settings.is_freekassa_enabled(): - logger.info(f" - Freekassa webhook: POST {settings.FREEKASSA_WEBHOOK_PATH}") logger.info(f" - Health check: GET /health") return self.app @@ -453,81 +446,7 @@ class WebhookServer: "service": "payment-webhooks", "tribute_enabled": settings.TRIBUTE_ENABLED, "cryptobot_enabled": settings.is_cryptobot_enabled(), - "freekassa_enabled": settings.is_freekassa_enabled(), "port": settings.TRIBUTE_WEBHOOK_PORT, "tribute_path": settings.TRIBUTE_WEBHOOK_PATH, - "cryptobot_path": settings.CRYPTOBOT_WEBHOOK_PATH if settings.is_cryptobot_enabled() else None, - "freekassa_path": settings.FREEKASSA_WEBHOOK_PATH if settings.is_freekassa_enabled() else None, + "cryptobot_path": settings.CRYPTOBOT_WEBHOOK_PATH if settings.is_cryptobot_enabled() else None }) - - async def _freekassa_webhook_handler(self, request: web.Request) -> web.Response: - """ - Обработчик webhook от Freekassa. - - Freekassa отправляет POST запрос с form-data: - - MERCHANT_ID: ID магазина - - AMOUNT: Сумма платежа - - MERCHANT_ORDER_ID: Наш order_id - - SIGN: Подпись MD5(shop_id:amount:secret2:order_id) - - intid: ID транзакции Freekassa - - CUR_ID: ID валюты/платежной системы - """ - try: - logger.info(f"Получен Freekassa webhook: {request.method} {request.path}") - - # Получаем IP клиента - client_ip = request.headers.get("X-Forwarded-For", "").split(",")[0].strip() - if not client_ip: - client_ip = request.remote or "unknown" - logger.info(f"Freekassa webhook IP: {client_ip}") - - # Freekassa отправляет form-data - try: - form_data = await request.post() - except Exception as e: - logger.error(f"Ошибка парсинга Freekassa form-data: {e}") - return web.Response(text="NO", status=400) - - logger.info(f"Freekassa webhook data: {dict(form_data)}") - - # Извлекаем параметры - merchant_id = int(form_data.get("MERCHANT_ID", 0)) - amount = float(form_data.get("AMOUNT", 0)) - order_id = form_data.get("MERCHANT_ORDER_ID", "") - sign = form_data.get("SIGN", "") - intid = form_data.get("intid", "") - cur_id = form_data.get("CUR_ID") - - if not order_id or not sign: - logger.warning("Freekassa webhook: отсутствуют обязательные параметры") - return web.Response(text="NO", status=400) - - # Обрабатываем платеж через PaymentService - from app.services.payment_service import PaymentService - from app.database.database import AsyncSessionLocal - - payment_service = PaymentService(self.bot) - - async with AsyncSessionLocal() as db: - success = await payment_service.process_freekassa_webhook( - db=db, - merchant_id=merchant_id, - amount=amount, - order_id=order_id, - sign=sign, - intid=intid, - cur_id=int(cur_id) if cur_id else None, - client_ip=client_ip, - ) - - if success: - logger.info(f"Freekassa webhook обработан успешно: order_id={order_id}") - # Freekassa ожидает YES в ответе - return web.Response(text="YES", status=200) - else: - logger.error(f"Ошибка обработки Freekassa webhook: order_id={order_id}") - return web.Response(text="NO", status=400) - - except Exception as e: - logger.error(f"Критическая ошибка обработки Freekassa webhook: {e}", exc_info=True) - return web.Response(text="NO", status=500) diff --git a/app/handlers/admin/bot_configuration.py b/app/handlers/admin/bot_configuration.py index cb9de4e9..87ccf94d 100644 --- a/app/handlers/admin/bot_configuration.py +++ b/app/handlers/admin/bot_configuration.py @@ -61,7 +61,7 @@ CATEGORY_GROUP_METADATA: Dict[str, Dict[str, object]] = { }, "payments": { "title": "💳 Платежные системы", - "description": "YooKassa, CryptoBot, Heleket, CloudPayments, Freekassa, MulenPay, PAL24, Wata, Platega, Tribute и Telegram Stars.", + "description": "YooKassa, CryptoBot, Heleket, CloudPayments, MulenPay, PAL24, Wata, Platega, Tribute и Telegram Stars.", "icon": "💳", "categories": ( "PAYMENT", @@ -70,7 +70,6 @@ CATEGORY_GROUP_METADATA: Dict[str, Dict[str, object]] = { "CRYPTOBOT", "HELEKET", "CLOUDPAYMENTS", - "FREEKASSA", "MULENPAY", "PAL24", "WATA", @@ -258,7 +257,6 @@ def _get_group_status(group_key: str) -> Tuple[str, str]: "CryptoBot": settings.is_cryptobot_enabled(), "Platega": settings.is_platega_enabled(), "CloudPayments": settings.is_cloudpayments_enabled(), - "Freekassa": settings.is_freekassa_enabled(), "MulenPay": settings.is_mulenpay_enabled(), "PAL24": settings.is_pal24_enabled(), "Tribute": settings.TRIBUTE_ENABLED, @@ -1336,9 +1334,6 @@ def _build_settings_keyboard( elif category_key == "CRYPTOBOT": label = texts.t("PAYMENT_CRYPTOBOT", "🪙 Криптовалюта (CryptoBot)") test_payment_buttons.append([_test_button(f"{label} · тест", "cryptobot")]) - elif category_key == "FREEKASSA": - label = texts.t("PAYMENT_FREEKASSA", "💳 Freekassa") - test_payment_buttons.append([_test_button(f"{label} · тест", "freekassa")]) if test_payment_buttons: rows.extend(test_payment_buttons) @@ -2336,47 +2331,6 @@ async def test_payment_provider( await _refresh_markup() return - if method == "freekassa": - if not settings.is_freekassa_enabled(): - await callback.answer("❌ Freekassa отключена", show_alert=True) - return - - amount_kopeks = settings.FREEKASSA_MIN_AMOUNT_KOPEKS - payment_result = await payment_service.create_freekassa_payment( - db=db, - user_id=db_user.id, - amount_kopeks=amount_kopeks, - description="Тестовый платеж Freekassa (админ)", - email=getattr(db_user, "email", None), - language=db_user.language or settings.DEFAULT_LANGUAGE, - ) - - if not payment_result or not payment_result.get("payment_url"): - await callback.answer("❌ Не удалось создать тестовый платеж Freekassa", show_alert=True) - await _refresh_markup() - return - - payment_url = payment_result["payment_url"] - message_text = ( - "🧪 Тестовый платеж Freekassa\n\n" - f"💰 Сумма: {texts.format_price(amount_kopeks)}\n" - f"🆔 Order ID: {payment_result['order_id']}" - ) - reply_markup = types.InlineKeyboardMarkup( - inline_keyboard=[ - [ - types.InlineKeyboardButton( - text="💳 Перейти к оплате", - url=payment_url, - ) - ] - ] - ) - await callback.message.answer(message_text, reply_markup=reply_markup, parse_mode="HTML") - await callback.answer("✅ Ссылка на платеж Freekassa отправлена", show_alert=True) - await _refresh_markup() - return - await callback.answer("❌ Неизвестный способ тестирования платежа", show_alert=True) await _refresh_markup() diff --git a/app/handlers/balance/freekassa.py b/app/handlers/balance/freekassa.py deleted file mode 100644 index 0f304b86..00000000 --- a/app/handlers/balance/freekassa.py +++ /dev/null @@ -1,391 +0,0 @@ -"""Handler for Freekassa balance top-up.""" - -import logging - -from aiogram import types -from aiogram.fsm.context import FSMContext -from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton -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 -from app.states import BalanceStates -from app.utils.decorators import error_handler - -logger = logging.getLogger(__name__) - - -async def _create_freekassa_payment_and_respond( - message_or_callback, - db_user: User, - db: AsyncSession, - amount_kopeks: int, - edit_message: bool = False, -): - """ - Common logic for creating Freekassa payment and sending response. - - Args: - message_or_callback: Either a Message or CallbackQuery object - db_user: User object - db: Database session - amount_kopeks: Amount in kopeks - edit_message: Whether to edit existing message or send new one - """ - texts = get_texts(db_user.language) - amount_rub = amount_kopeks / 100 - - # Create payment - payment_service = PaymentService() - - description = settings.PAYMENT_BALANCE_TEMPLATE.format( - service_name=settings.PAYMENT_SERVICE_NAME, - description="Пополнение баланса", - ) - - result = await payment_service.create_freekassa_payment( - db=db, - user_id=db_user.id, - amount_kopeks=amount_kopeks, - description=description, - email=getattr(db_user, "email", None), - language=db_user.language, - ) - - if not result: - error_text = texts.t( - "PAYMENT_CREATE_ERROR", - "Не удалось создать платёж. Попробуйте позже.", - ) - if edit_message: - await message_or_callback.edit_text( - error_text, - reply_markup=get_back_keyboard(db_user.language), - parse_mode="HTML", - ) - else: - await message_or_callback.answer( - error_text, - parse_mode="HTML", - ) - return - - payment_url = result.get("payment_url") - display_name = settings.get_freekassa_display_name() - - # Create keyboard with payment button - keyboard = InlineKeyboardMarkup( - inline_keyboard=[ - [ - InlineKeyboardButton( - text=texts.t( - "PAY_BUTTON", - "💳 Оплатить {amount}₽", - ).format(amount=f"{amount_rub:.0f}"), - url=payment_url, - ) - ], - [ - InlineKeyboardButton( - text=texts.t("BACK_BUTTON", "◀️ Назад"), - callback_data="menu_balance", - ) - ], - ] - ) - - response_text = texts.t( - "FREEKASSA_PAYMENT_CREATED", - "💳 Оплата через {name}\n\n" - "Сумма: {amount}₽\n\n" - "Нажмите кнопку ниже для оплаты.\n" - "После успешной оплаты баланс будет пополнен автоматически.", - ).format(name=display_name, amount=f"{amount_rub:.2f}") - - if edit_message: - await message_or_callback.edit_text( - response_text, - reply_markup=keyboard, - parse_mode="HTML", - ) - else: - await message_or_callback.answer( - response_text, - reply_markup=keyboard, - parse_mode="HTML", - ) - - logger.info( - "Freekassa payment created: user=%s, amount=%s₽", - db_user.telegram_id, - amount_rub, - ) - - -@error_handler -async def process_freekassa_payment_amount( - message: types.Message, - db_user: User, - db: AsyncSession, - amount_kopeks: int, - state: FSMContext, -): - """ - Process payment amount directly (called from quick_amount handlers). - """ - texts = get_texts(db_user.language) - - # Проверка ограничения на пополнение - if getattr(db_user, "restriction_topup", False): - reason = ( - getattr(db_user, "restriction_reason", None) - or "Действие ограничено администратором" - ) - support_url = settings.get_support_contact_url() - keyboard = [] - if support_url: - keyboard.append( - [InlineKeyboardButton(text="🆘 Обжаловать", url=support_url)] - ) - keyboard.append( - [InlineKeyboardButton(text=texts.BACK, callback_data="menu_balance")] - ) - - await message.answer( - f"🚫 Пополнение ограничено\n\n{reason}", - parse_mode="HTML", - reply_markup=InlineKeyboardMarkup(inline_keyboard=keyboard), - ) - await state.clear() - return - - # Validate amount - min_amount = settings.FREEKASSA_MIN_AMOUNT_KOPEKS - max_amount = settings.FREEKASSA_MAX_AMOUNT_KOPEKS - - if amount_kopeks < min_amount: - await message.answer( - texts.t( - "PAYMENT_AMOUNT_TOO_LOW", - "Минимальная сумма пополнения: {min_amount}₽", - ).format(min_amount=min_amount // 100), - parse_mode="HTML", - ) - return - - if amount_kopeks > max_amount: - await message.answer( - texts.t( - "PAYMENT_AMOUNT_TOO_HIGH", - "Максимальная сумма пополнения: {max_amount}₽", - ).format(max_amount=max_amount // 100), - parse_mode="HTML", - ) - return - - await state.clear() - - await _create_freekassa_payment_and_respond( - message_or_callback=message, - db_user=db_user, - db=db, - amount_kopeks=amount_kopeks, - edit_message=False, - ) - - -@error_handler -async def start_freekassa_topup( - callback: types.CallbackQuery, - db_user: User, - db: AsyncSession, - state: FSMContext, -): - """ - Start Freekassa top-up process - ask for amount. - """ - texts = get_texts(db_user.language) - - # Проверка ограничения на пополнение - if getattr(db_user, "restriction_topup", False): - reason = ( - getattr(db_user, "restriction_reason", None) - or "Действие ограничено администратором" - ) - support_url = settings.get_support_contact_url() - keyboard = [] - if support_url: - keyboard.append( - [InlineKeyboardButton(text="🆘 Обжаловать", url=support_url)] - ) - keyboard.append( - [InlineKeyboardButton(text=texts.BACK, callback_data="menu_balance")] - ) - - await callback.message.edit_text( - f"🚫 Пополнение ограничено\n\n{reason}", - parse_mode="HTML", - reply_markup=InlineKeyboardMarkup(inline_keyboard=keyboard), - ) - return - - await state.set_state(BalanceStates.waiting_for_amount) - await state.update_data(payment_method="freekassa") - - min_amount = settings.FREEKASSA_MIN_AMOUNT_KOPEKS // 100 - max_amount = settings.FREEKASSA_MAX_AMOUNT_KOPEKS // 100 - display_name = settings.get_freekassa_display_name() - - keyboard = InlineKeyboardMarkup( - inline_keyboard=[ - [ - InlineKeyboardButton( - text=texts.t("BACK_BUTTON", "◀️ Назад"), - callback_data="menu_balance", - ) - ] - ] - ) - - await callback.message.edit_text( - texts.t( - "FREEKASSA_ENTER_AMOUNT", - "💳 Пополнение через {name}\n\n" - "Введите сумму пополнения в рублях.\n\n" - "Минимум: {min_amount}₽\n" - "Максимум: {max_amount}₽", - ).format( - name=display_name, - min_amount=min_amount, - max_amount=f"{max_amount:,}".replace(",", " "), - ), - parse_mode="HTML", - reply_markup=keyboard, - ) - - -@error_handler -async def process_freekassa_custom_amount( - message: types.Message, - db_user: User, - db: AsyncSession, - state: FSMContext, -): - """ - Process custom amount input for Freekassa payment. - """ - data = await state.get_data() - if data.get("payment_method") != "freekassa": - return - - texts = get_texts(db_user.language) - - try: - amount_text = message.text.replace(",", ".").replace(" ", "").strip() - amount_rubles = float(amount_text) - amount_kopeks = int(amount_rubles * 100) - except (ValueError, TypeError): - await message.answer( - texts.t( - "PAYMENT_INVALID_AMOUNT", - "Введите корректную сумму числом.", - ), - parse_mode="HTML", - ) - return - - await process_freekassa_payment_amount( - message=message, - db_user=db_user, - db=db, - amount_kopeks=amount_kopeks, - state=state, - ) - - -@error_handler -async def process_freekassa_quick_amount( - callback: types.CallbackQuery, - db_user: User, - db: AsyncSession, - state: FSMContext, -): - """ - Process quick amount selection for Freekassa payment. - Called when user clicks a predefined amount button. - """ - texts = get_texts(db_user.language) - - if not settings.is_freekassa_enabled(): - await callback.answer( - texts.t("FREEKASSA_NOT_AVAILABLE", "Freekassa временно недоступен"), - show_alert=True, - ) - return - - # Extract amount from callback data: topup_amount|freekassa|{amount_kopeks} - try: - parts = callback.data.split("|") - if len(parts) >= 3: - amount_kopeks = int(parts[2]) - else: - await callback.answer("Invalid callback data", show_alert=True) - return - except (ValueError, IndexError): - await callback.answer("Invalid amount", show_alert=True) - return - - # Проверка ограничения на пополнение - if getattr(db_user, "restriction_topup", False): - reason = ( - getattr(db_user, "restriction_reason", None) - or "Действие ограничено администратором" - ) - support_url = settings.get_support_contact_url() - keyboard = [] - if support_url: - keyboard.append( - [InlineKeyboardButton(text="🆘 Обжаловать", url=support_url)] - ) - keyboard.append( - [InlineKeyboardButton(text=texts.BACK, callback_data="menu_balance")] - ) - - await callback.message.edit_text( - f"🚫 Пополнение ограничено\n\n{reason}", - parse_mode="HTML", - reply_markup=InlineKeyboardMarkup(inline_keyboard=keyboard), - ) - return - - # Validate amount - min_amount = settings.FREEKASSA_MIN_AMOUNT_KOPEKS - max_amount = settings.FREEKASSA_MAX_AMOUNT_KOPEKS - - if amount_kopeks < min_amount: - await callback.answer( - texts.t("AMOUNT_TOO_LOW_SHORT", "Сумма слишком мала"), - show_alert=True, - ) - return - - if amount_kopeks > max_amount: - await callback.answer( - texts.t("AMOUNT_TOO_HIGH_SHORT", "Сумма слишком велика"), - show_alert=True, - ) - return - - await callback.answer() - await state.clear() - - await _create_freekassa_payment_and_respond( - message_or_callback=callback.message, - db_user=db_user, - db=db, - amount_kopeks=amount_kopeks, - edit_message=True, - ) diff --git a/app/handlers/balance/main.py b/app/handlers/balance/main.py index 7d57e0c7..a131278e 100644 --- a/app/handlers/balance/main.py +++ b/app/handlers/balance/main.py @@ -111,12 +111,6 @@ async def route_payment_by_method( await process_cloudpayments_payment_amount(message, db_user, db, amount_kopeks, state) return True - if payment_method == "freekassa": - from .freekassa import process_freekassa_payment_amount - async with AsyncSessionLocal() as db: - await process_freekassa_payment_amount(message, db_user, db, amount_kopeks, state) - return True - return False @@ -928,16 +922,6 @@ def register_balance_handlers(dp: Dispatcher): F.data.startswith("topup_amount|cloudpayments|") ) - from .freekassa import start_freekassa_topup, process_freekassa_quick_amount - dp.callback_query.register( - start_freekassa_topup, - F.data == "topup_freekassa" - ) - dp.callback_query.register( - process_freekassa_quick_amount, - F.data.startswith("topup_amount|freekassa|") - ) - from .mulenpay import check_mulenpay_payment_status dp.callback_query.register( check_mulenpay_payment_status, diff --git a/app/keyboards/inline.py b/app/keyboards/inline.py index 62586101..8ac064d0 100644 --- a/app/keyboards/inline.py +++ b/app/keyboards/inline.py @@ -1429,16 +1429,6 @@ def get_payment_methods_keyboard(amount_kopeks: int, language: str = DEFAULT_LAN ]) has_direct_payment_methods = True - if settings.is_freekassa_enabled(): - freekassa_name = settings.get_freekassa_display_name() - keyboard.append([ - InlineKeyboardButton( - text=texts.t("PAYMENT_FREEKASSA", f"💳 {freekassa_name}"), - callback_data=_build_callback("freekassa") - ) - ]) - has_direct_payment_methods = True - if settings.is_support_topup_enabled(): keyboard.append([ InlineKeyboardButton( diff --git a/app/services/freekassa_service.py b/app/services/freekassa_service.py deleted file mode 100644 index 3afc917d..00000000 --- a/app/services/freekassa_service.py +++ /dev/null @@ -1,254 +0,0 @@ -"""Сервис для работы с API Freekassa.""" - -import hashlib -import time -import logging -from typing import Optional, Dict, Any, Set - -import aiohttp - -from app.config import settings - -logger = logging.getLogger(__name__) - -# IP-адреса Freekassa для проверки webhook -FREEKASSA_IPS: Set[str] = { - "168.119.157.136", - "168.119.60.227", - "178.154.197.79", - "51.250.54.238", -} - -API_BASE_URL = "https://api.fk.life/v1" - - -class FreekassaService: - """Сервис для работы с API Freekassa.""" - - def __init__(self): - self._shop_id: Optional[int] = None - self._api_key: Optional[str] = None - self._secret1: Optional[str] = None - self._secret2: Optional[str] = None - - @property - def shop_id(self) -> int: - if self._shop_id is None: - self._shop_id = settings.FREEKASSA_SHOP_ID - return self._shop_id or 0 - - @property - def api_key(self) -> str: - if self._api_key is None: - self._api_key = settings.FREEKASSA_API_KEY - return self._api_key or "" - - @property - def secret1(self) -> str: - if self._secret1 is None: - self._secret1 = settings.FREEKASSA_SECRET_WORD_1 - return self._secret1 or "" - - @property - def secret2(self) -> str: - if self._secret2 is None: - self._secret2 = settings.FREEKASSA_SECRET_WORD_2 - return self._secret2 or "" - - def _generate_api_signature(self, params: Dict[str, Any]) -> str: - """ - Генерирует подпись для API запроса. - Сортировка по ключам, конкатенация значений через | - """ - sorted_keys = sorted(params.keys()) - values = [str(params[k]) for k in sorted_keys if params[k] is not None] - sign_string = "|".join(values) - return hashlib.md5(sign_string.encode()).hexdigest() - - def generate_form_signature( - self, amount: float, currency: str, order_id: str - ) -> str: - """ - Генерирует подпись для платежной формы. - Формат: MD5(shop_id:amount:secret1:currency:order_id) - """ - sign_string = f"{self.shop_id}:{amount}:{self.secret1}:{currency}:{order_id}" - return hashlib.md5(sign_string.encode()).hexdigest() - - def verify_webhook_signature( - self, shop_id: int, amount: float, order_id: str, sign: str - ) -> bool: - """ - Проверяет подпись webhook уведомления. - Формат: MD5(shop_id:amount:secret2:order_id) - """ - expected_sign = hashlib.md5( - f"{shop_id}:{amount}:{self.secret2}:{order_id}".encode() - ).hexdigest() - return sign.lower() == expected_sign.lower() - - def verify_webhook_ip(self, ip: str) -> bool: - """Проверяет, что IP входит в разрешенный список Freekassa.""" - return ip in FREEKASSA_IPS - - def build_payment_url( - self, - order_id: str, - amount: float, - currency: str = "RUB", - email: Optional[str] = None, - phone: Optional[str] = None, - payment_system_id: Optional[int] = None, - lang: str = "ru", - ) -> str: - """ - Формирует URL для перенаправления на оплату. - """ - signature = self.generate_form_signature(amount, currency, order_id) - - params = { - "m": self.shop_id, - "oa": amount, - "currency": currency, - "o": order_id, - "s": signature, - "lang": lang, - } - - if email: - params["em"] = email - if phone: - params["phone"] = phone - if payment_system_id: - params["i"] = payment_system_id - - query = "&".join(f"{k}={v}" for k, v in params.items()) - return f"https://pay.freekassa.ru/?{query}" - - async def create_order( - self, - order_id: str, - amount: float, - currency: str = "RUB", - email: Optional[str] = None, - ip: Optional[str] = None, - payment_system_id: Optional[int] = None, - success_url: Optional[str] = None, - failure_url: Optional[str] = None, - notification_url: Optional[str] = None, - ) -> Dict[str, Any]: - """ - Создает заказ через API Freekassa. - POST /orders/create - """ - params = { - "shopId": self.shop_id, - "nonce": int(time.time() * 1000), - "paymentId": order_id, - "i": payment_system_id or 1, - "email": email or "user@example.com", - "ip": ip or "127.0.0.1", - "amount": amount, - "currency": currency, - } - - if success_url: - params["success_url"] = success_url - if failure_url: - params["failure_url"] = failure_url - if notification_url: - params["notification_url"] = notification_url - - params["signature"] = self._generate_api_signature(params) - - try: - async with aiohttp.ClientSession() as session: - async with session.post( - f"{API_BASE_URL}/orders/create", - json=params, - headers={"Content-Type": "application/json"}, - timeout=aiohttp.ClientTimeout(total=30), - ) as response: - data = await response.json() - - if response.status != 200 or data.get("type") == "error": - logger.error(f"Freekassa create_order error: {data}") - raise Exception( - f"Freekassa API error: {data.get('message', 'Unknown error')}" - ) - - return data - except aiohttp.ClientError as e: - logger.exception(f"Freekassa API connection error: {e}") - raise - - async def get_order_status(self, order_id: str) -> Dict[str, Any]: - """ - Получает статус заказа. - POST /orders - """ - params = { - "shopId": self.shop_id, - "nonce": int(time.time() * 1000), - "paymentId": order_id, - } - params["signature"] = self._generate_api_signature(params) - - try: - async with aiohttp.ClientSession() as session: - async with session.post( - f"{API_BASE_URL}/orders", - json=params, - headers={"Content-Type": "application/json"}, - timeout=aiohttp.ClientTimeout(total=30), - ) as response: - return await response.json() - except aiohttp.ClientError as e: - logger.exception(f"Freekassa API connection error: {e}") - raise - - async def get_balance(self) -> Dict[str, Any]: - """Получает баланс магазина.""" - params = { - "shopId": self.shop_id, - "nonce": int(time.time() * 1000), - } - params["signature"] = self._generate_api_signature(params) - - try: - async with aiohttp.ClientSession() as session: - async with session.post( - f"{API_BASE_URL}/balance", - json=params, - headers={"Content-Type": "application/json"}, - timeout=aiohttp.ClientTimeout(total=30), - ) as response: - return await response.json() - except aiohttp.ClientError as e: - logger.exception(f"Freekassa API connection error: {e}") - raise - - async def get_payment_systems(self) -> Dict[str, Any]: - """Получает список доступных платежных систем.""" - params = { - "shopId": self.shop_id, - "nonce": int(time.time() * 1000), - } - params["signature"] = self._generate_api_signature(params) - - try: - async with aiohttp.ClientSession() as session: - async with session.post( - f"{API_BASE_URL}/currencies", - json=params, - headers={"Content-Type": "application/json"}, - timeout=aiohttp.ClientTimeout(total=30), - ) as response: - return await response.json() - except aiohttp.ClientError as e: - logger.exception(f"Freekassa API connection error: {e}") - raise - - -# Singleton instance -freekassa_service = FreekassaService() diff --git a/app/services/payment/__init__.py b/app/services/payment/__init__.py index 1f1a53f1..9e50718b 100644 --- a/app/services/payment/__init__.py +++ b/app/services/payment/__init__.py @@ -15,7 +15,6 @@ from .pal24 import Pal24PaymentMixin from .platega import PlategaPaymentMixin from .wata import WataPaymentMixin from .cloudpayments import CloudPaymentsPaymentMixin -from .freekassa import FreekassaPaymentMixin __all__ = [ "PaymentCommonMixin", @@ -29,5 +28,4 @@ __all__ = [ "PlategaPaymentMixin", "WataPaymentMixin", "CloudPaymentsPaymentMixin", - "FreekassaPaymentMixin", ] diff --git a/app/services/payment/freekassa.py b/app/services/payment/freekassa.py deleted file mode 100644 index b688a8aa..00000000 --- a/app/services/payment/freekassa.py +++ /dev/null @@ -1,516 +0,0 @@ -"""Mixin для интеграции с Freekassa.""" - -from __future__ import annotations - -import json -import uuid -import logging -from datetime import datetime, timedelta -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.freekassa_service import freekassa_service -from app.services.subscription_auto_purchase_service import ( - auto_activate_subscription_after_topup, - auto_purchase_saved_cart_after_topup, -) -from app.utils.user_utils import format_referrer_info -from app.utils.payment_logger import payment_logger as logger - - -class FreekassaPaymentMixin: - """Mixin для работы с платежами Freekassa.""" - - async def create_freekassa_payment( - self, - db: AsyncSession, - *, - user_id: int, - amount_kopeks: int, - description: str = "Пополнение баланса", - email: Optional[str] = None, - language: str = "ru", - ) -> Optional[Dict[str, Any]]: - """ - Создает платеж Freekassa. - - Args: - db: Сессия БД - user_id: ID пользователя - amount_kopeks: Сумма в копейках - description: Описание платежа - email: Email пользователя - language: Язык интерфейса - - Returns: - Словарь с данными платежа или None при ошибке - """ - if not settings.is_freekassa_enabled(): - logger.error("Freekassa не настроен") - return None - - # Валидация лимитов - if amount_kopeks < settings.FREEKASSA_MIN_AMOUNT_KOPEKS: - logger.warning( - "Freekassa: сумма %s меньше минимальной %s", - amount_kopeks, - settings.FREEKASSA_MIN_AMOUNT_KOPEKS, - ) - return None - - if amount_kopeks > settings.FREEKASSA_MAX_AMOUNT_KOPEKS: - logger.warning( - "Freekassa: сумма %s больше максимальной %s", - amount_kopeks, - settings.FREEKASSA_MAX_AMOUNT_KOPEKS, - ) - return None - - # Генерируем уникальный order_id - order_id = f"fk_{user_id}_{uuid.uuid4().hex[:12]}" - amount_rubles = amount_kopeks / 100 - currency = settings.FREEKASSA_CURRENCY - - # Срок действия платежа - expires_at = datetime.utcnow() + timedelta( - seconds=settings.FREEKASSA_PAYMENT_TIMEOUT_SECONDS - ) - - # Метаданные - metadata = { - "user_id": user_id, - "amount_kopeks": amount_kopeks, - "description": description, - "language": language, - "type": "balance_topup", - } - - try: - # Генерируем URL для оплаты - payment_url = freekassa_service.build_payment_url( - order_id=order_id, - amount=amount_rubles, - currency=currency, - email=email, - lang=language, - ) - - # Импортируем CRUD модуль - freekassa_crud = import_module("app.database.crud.freekassa") - - # Сохраняем в БД - local_payment = await freekassa_crud.create_freekassa_payment( - db=db, - user_id=user_id, - order_id=order_id, - amount_kopeks=amount_kopeks, - currency=currency, - description=description, - payment_url=payment_url, - expires_at=expires_at, - metadata_json=json.dumps(metadata, ensure_ascii=False), - ) - - logger.info( - "Freekassa: создан платеж order_id=%s, user_id=%s, amount=%s %s", - order_id, - user_id, - amount_rubles, - currency, - ) - - return { - "order_id": order_id, - "amount_kopeks": amount_kopeks, - "amount_rubles": amount_rubles, - "currency": currency, - "payment_url": payment_url, - "expires_at": expires_at.isoformat(), - "local_payment_id": local_payment.id, - } - - except Exception as e: - logger.exception("Freekassa: ошибка создания платежа: %s", e) - return None - - async def process_freekassa_webhook( - self, - db: AsyncSession, - *, - merchant_id: int, - amount: float, - order_id: str, - sign: str, - intid: str, - cur_id: Optional[int] = None, - client_ip: str, - ) -> bool: - """ - Обрабатывает webhook от Freekassa. - - Args: - db: Сессия БД - merchant_id: ID магазина (MERCHANT_ID) - amount: Сумма платежа (AMOUNT) - order_id: Номер заказа (MERCHANT_ORDER_ID) - sign: Подпись (SIGN) - intid: ID транзакции Freekassa - cur_id: ID валюты/платежной системы (CUR_ID) - client_ip: IP клиента - - Returns: - True если платеж успешно обработан - """ - try: - # Проверка IP - if not freekassa_service.verify_webhook_ip(client_ip): - logger.warning("Freekassa webhook: недоверенный IP %s", client_ip) - return False - - # Проверка подписи - if not freekassa_service.verify_webhook_signature( - merchant_id, amount, order_id, sign - ): - logger.warning( - "Freekassa webhook: неверная подпись для order_id=%s", order_id - ) - return False - - # Импортируем CRUD модуль - freekassa_crud = import_module("app.database.crud.freekassa") - - # Получаем платеж из БД - payment = await freekassa_crud.get_freekassa_payment_by_order_id( - db, order_id - ) - if not payment: - logger.warning( - "Freekassa webhook: платеж не найден order_id=%s", order_id - ) - return False - - # Проверка дублирования - if payment.is_paid: - logger.info( - "Freekassa webhook: платеж уже обработан order_id=%s", order_id - ) - return True - - # Проверка суммы - expected_amount = payment.amount_kopeks / 100 - if abs(amount - expected_amount) > 0.01: - logger.warning( - "Freekassa webhook: несоответствие суммы ожидалось=%s, получено=%s", - expected_amount, - amount, - ) - return False - - # Обновляем статус платежа - callback_payload = { - "merchant_id": merchant_id, - "amount": amount, - "order_id": order_id, - "intid": intid, - "cur_id": cur_id, - } - - payment = await freekassa_crud.update_freekassa_payment_status( - db=db, - payment=payment, - status="success", - is_paid=True, - freekassa_order_id=intid, - payment_system_id=cur_id, - callback_payload=callback_payload, - ) - - # Финализируем платеж (начисляем баланс, создаем транзакцию) - return await self._finalize_freekassa_payment( - db, payment, intid=intid, trigger="webhook" - ) - - except Exception as e: - logger.exception("Freekassa webhook: ошибка обработки: %s", e) - return False - - async def _finalize_freekassa_payment( - self, - db: AsyncSession, - payment: Any, - *, - intid: Optional[str], - trigger: str, - ) -> bool: - """Создаёт транзакцию, начисляет баланс и отправляет уведомления.""" - payment_module = import_module("app.services.payment_service") - - if payment.transaction_id: - logger.info( - "Freekassa платеж %s уже привязан к транзакции (trigger=%s)", - payment.order_id, - trigger, - ) - return True - - # Получаем пользователя - user = await payment_module.get_user_by_id(db, payment.user_id) - if not user: - logger.error( - "Пользователь %s не найден для Freekassa платежа %s (trigger=%s)", - payment.user_id, - payment.order_id, - trigger, - ) - return False - - # Создаем транзакцию - transaction = await payment_module.create_transaction( - db, - user_id=payment.user_id, - type=TransactionType.DEPOSIT, - amount_kopeks=payment.amount_kopeks, - description=f"Пополнение через Freekassa (#{intid or payment.order_id})", - payment_method=PaymentMethod.FREEKASSA, - external_id=str(intid) if intid else payment.order_id, - is_completed=True, - ) - - # Связываем платеж с транзакцией - freekassa_crud = import_module("app.database.crud.freekassa") - await freekassa_crud.update_freekassa_payment_status( - db=db, - payment=payment, - status=payment.status, - transaction_id=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() - - promo_group = user.get_primary_promo_group() - subscription = getattr(user, "subscription", None) - referrer_info = format_referrer_info(user) - topup_status = "Первое пополнение" if was_first_topup else "Пополнение" - - await db.commit() - - # Обработка реферального пополнения - 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( - "Ошибка обработки реферального пополнения Freekassa: %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) - await db.refresh(payment) - - # Отправка уведомления админам - 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( - "Ошибка отправки админ уведомления Freekassa: %s", error - ) - - # Отправка уведомления пользователю - if getattr(self, "bot", None): - try: - keyboard = await self.build_topup_success_keyboard(user) - display_name = settings.get_freekassa_display_name() - await self.bot.send_message( - user.telegram_id, - ( - "✅ Пополнение успешно!\n\n" - f"💰 Сумма: {settings.format_price(payment.amount_kopeks)}\n" - f"💳 Способ: {display_name}\n" - f"🆔 Транзакция: {transaction.id}\n\n" - "Баланс пополнен автоматически!" - ), - parse_mode="HTML", - reply_markup=keyboard, - ) - except Exception as error: - logger.error( - "Ошибка отправки уведомления пользователю Freekassa: %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) - auto_purchase_success = False - - if has_saved_cart: - try: - auto_purchase_success = await auto_purchase_saved_cart_after_topup( - db, - user, - bot=getattr(self, "bot", None), - ) - except Exception as auto_error: - logger.error( - "Ошибка автоматической покупки подписки для пользователя %s: %s", - user.id, - auto_error, - exc_info=True, - ) - - if auto_purchase_success: - has_saved_cart = False - - # Умная автоактивация если автопокупка не сработала - if not auto_purchase_success: - try: - await auto_activate_subscription_after_topup(db, user) - except Exception as auto_activate_error: - logger.error( - "Ошибка умной автоактивации для пользователя %s: %s", - user.id, - auto_activate_error, - exc_info=True, - ) - - 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", - "У вас есть незавершенное оформление подписки. Вернуться?", - ) - - keyboard = types.InlineKeyboardMarkup( - inline_keyboard=[ - [ - types.InlineKeyboardButton( - text=texts.t( - "BALANCE_TOPUP_CART_BUTTON", - "🛒 Продолжить оформление", - ), - callback_data="return_to_saved_cart", - ) - ], - [ - types.InlineKeyboardButton( - text="🏠 Главное меню", - callback_data="back_to_menu", - ) - ], - ] - ) - - await self.bot.send_message( - chat_id=user.telegram_id, - text=( - "✅ Баланс пополнен на " - f"{settings.format_price(payment.amount_kopeks)}!\n\n" - f"{cart_message}" - ), - reply_markup=keyboard, - ) - except Exception as error: - logger.error( - "Ошибка при работе с сохраненной корзиной для пользователя %s: %s", - user.id, - error, - exc_info=True, - ) - - logger.info( - "✅ Обработан Freekassa платеж %s для пользователя %s (trigger=%s)", - payment.order_id, - payment.user_id, - trigger, - ) - - return True - - async def check_freekassa_payment_status( - self, - db: AsyncSession, - order_id: str, - ) -> Optional[Dict[str, Any]]: - """ - Проверяет статус платежа через API. - - Args: - db: Сессия БД - order_id: Номер заказа - - Returns: - Данные о статусе платежа - """ - try: - status_data = await freekassa_service.get_order_status(order_id) - return status_data - except Exception as e: - logger.exception("Freekassa: ошибка проверки статуса: %s", e) - return None - - async def get_freekassa_payment_status( - self, - db: AsyncSession, - local_payment_id: int, - ) -> Optional[Dict[str, Any]]: - """ - Проверяет статус платежа Freekassa по локальному ID. - - Freekassa не предоставляет API для проверки статуса платежа, - поэтому возвращаем текущее состояние из БД. - - Args: - db: Сессия БД - local_payment_id: Внутренний ID платежа - - Returns: - Dict с информацией о платеже или None если не найден - """ - freekassa_crud = import_module("app.database.crud.freekassa") - - payment = await freekassa_crud.get_freekassa_payment_by_id(db, local_payment_id) - if not payment: - logger.warning("Freekassa payment not found: id=%s", local_payment_id) - return None - - # Freekassa не имеет API для проверки статуса, - # информация приходит только через webhook - return { - "payment": payment, - "status": payment.status or "pending", - "is_paid": payment.is_paid, - } diff --git a/app/services/payment_service.py b/app/services/payment_service.py index 1f068a48..e6559467 100644 --- a/app/services/payment_service.py +++ b/app/services/payment_service.py @@ -29,7 +29,6 @@ from app.services.payment import ( WataPaymentMixin, ) from app.services.payment.cloudpayments import CloudPaymentsPaymentMixin -from app.services.payment.freekassa import FreekassaPaymentMixin from app.services.yookassa_service import YooKassaService from app.services.wata_service import WataService from app.services.cloudpayments_service import CloudPaymentsService @@ -298,7 +297,6 @@ class PaymentService( PlategaPaymentMixin, WataPaymentMixin, CloudPaymentsPaymentMixin, - FreekassaPaymentMixin, ): """Основной интерфейс платежей, делегирующий работу специализированным mixin-ам.""" diff --git a/app/services/payment_verification_service.py b/app/services/payment_verification_service.py index af36028e..ce901630 100644 --- a/app/services/payment_verification_service.py +++ b/app/services/payment_verification_service.py @@ -19,7 +19,6 @@ from app.database.database import AsyncSessionLocal from app.database.models import ( CloudPaymentsPayment, CryptoBotPayment, - FreekassaPayment, HeleketPayment, MulenPayPayment, Pal24Payment, @@ -67,7 +66,6 @@ SUPPORTED_MANUAL_CHECK_METHODS: frozenset[PaymentMethod] = frozenset( PaymentMethod.CRYPTOBOT, PaymentMethod.PLATEGA, PaymentMethod.CLOUDPAYMENTS, - PaymentMethod.FREEKASSA, } ) @@ -81,7 +79,6 @@ SUPPORTED_AUTO_CHECK_METHODS: frozenset[PaymentMethod] = frozenset( PaymentMethod.CRYPTOBOT, PaymentMethod.PLATEGA, PaymentMethod.CLOUDPAYMENTS, - PaymentMethod.FREEKASSA, } ) @@ -103,8 +100,6 @@ def method_display_name(method: PaymentMethod) -> str: return "Heleket" if method == PaymentMethod.CLOUDPAYMENTS: return "CloudPayments" - if method == PaymentMethod.FREEKASSA: - return "Freekassa" if method == PaymentMethod.TELEGRAM_STARS: return "Telegram Stars" return method.value @@ -127,8 +122,6 @@ def _method_is_enabled(method: PaymentMethod) -> bool: return settings.is_heleket_enabled() if method == PaymentMethod.CLOUDPAYMENTS: return settings.is_cloudpayments_enabled() - if method == PaymentMethod.FREEKASSA: - return settings.is_freekassa_enabled() return False @@ -369,13 +362,6 @@ def _is_cloudpayments_pending(payment: CloudPaymentsPayment) -> bool: return status in {"pending", "authorized"} -def _is_freekassa_pending(payment: FreekassaPayment) -> bool: - if payment.is_paid: - return False - status = (payment.status or "").lower() - return status in {"pending", "created", "processing"} - - def _parse_cryptobot_amount_kopeks(payment: CryptoBotPayment) -> int: payload = payment.payload or "" match = re.search(r"_(\d+)$", payload) @@ -635,31 +621,6 @@ async def _fetch_cloudpayments_payments(db: AsyncSession, cutoff: datetime) -> L return records -async def _fetch_freekassa_payments(db: AsyncSession, cutoff: datetime) -> List[PendingPayment]: - stmt = ( - select(FreekassaPayment) - .options(selectinload(FreekassaPayment.user)) - .where(FreekassaPayment.created_at >= cutoff) - .order_by(desc(FreekassaPayment.created_at)) - ) - result = await db.execute(stmt) - records: List[PendingPayment] = [] - for payment in result.scalars().all(): - if not _is_freekassa_pending(payment): - continue - record = _build_record( - PaymentMethod.FREEKASSA, - payment, - identifier=payment.order_id, - amount_kopeks=payment.amount_kopeks, - status=payment.status or "", - is_paid=bool(payment.is_paid), - ) - if record: - records.append(record) - return records - - async def _fetch_stars_transactions(db: AsyncSession, cutoff: datetime) -> List[PendingPayment]: stmt = ( select(Transaction) @@ -705,7 +666,6 @@ async def list_recent_pending_payments( await _fetch_heleket_payments(db, cutoff), await _fetch_cryptobot_payments(db, cutoff), await _fetch_cloudpayments_payments(db, cutoff), - await _fetch_freekassa_payments(db, cutoff), await _fetch_stars_transactions(db, cutoff), ) @@ -846,20 +806,6 @@ async def get_payment_record( is_paid=bool(payment.is_paid), ) - if method == PaymentMethod.FREEKASSA: - payment = await db.get(FreekassaPayment, local_payment_id) - if not payment: - return None - await db.refresh(payment, attribute_names=["user"]) - return _build_record( - method, - payment, - identifier=payment.order_id, - amount_kopeks=payment.amount_kopeks, - status=payment.status or "", - is_paid=bool(payment.is_paid), - ) - if method == PaymentMethod.TELEGRAM_STARS: transaction = await db.get(Transaction, local_payment_id) if not transaction: @@ -914,9 +860,6 @@ async def run_manual_check( elif method == PaymentMethod.CLOUDPAYMENTS: result = await payment_service.get_cloudpayments_payment_status(db, local_payment_id) payment = result.get("payment") if result else None - elif method == PaymentMethod.FREEKASSA: - result = await payment_service.get_freekassa_payment_status(db, local_payment_id) - payment = result.get("payment") if result else None else: logger.warning("Manual check requested for unsupported method %s", method) return None diff --git a/app/services/system_settings_service.py b/app/services/system_settings_service.py index d7b8b787..ff2177a4 100644 --- a/app/services/system_settings_service.py +++ b/app/services/system_settings_service.py @@ -84,7 +84,6 @@ class BotConfigurationService: "CRYPTOBOT": "🪙 CryptoBot", "HELEKET": "🪙 Heleket", "CLOUDPAYMENTS": "💳 CloudPayments", - "FREEKASSA": "💳 Freekassa", "YOOKASSA": "🟣 YooKassa", "PLATEGA": "💳 {platega_name}", "TRIBUTE": "🎁 Tribute", @@ -141,7 +140,6 @@ class BotConfigurationService: "CRYPTOBOT": "CryptoBot и криптоплатежи через Telegram.", "HELEKET": "Heleket: криптоплатежи, ключи мерчанта и вебхуки.", "CLOUDPAYMENTS": "CloudPayments: оплата банковскими картами, Public ID, API Secret и вебхуки.", - "FREEKASSA": "Freekassa: ID магазина, API ключ, секретные слова и вебхуки.", "PLATEGA": "{platega_name}: merchant ID, секрет, ссылки возврата и методы оплаты.", "MULENPAY": "Платежи {mulenpay_name} и параметры магазина.", "PAL24": "PAL24 / PayPalych подключения и лимиты.", @@ -315,7 +313,6 @@ class BotConfigurationService: "CRYPTOBOT_": "CRYPTOBOT", "HELEKET_": "HELEKET", "CLOUDPAYMENTS_": "CLOUDPAYMENTS", - "FREEKASSA_": "FREEKASSA", "PLATEGA_": "PLATEGA", "MULENPAY_": "MULENPAY", "PAL24_": "PAL24", diff --git a/app/webapi/routes/miniapp.py b/app/webapi/routes/miniapp.py index 9aead7f1..4e4b733c 100644 --- a/app/webapi/routes/miniapp.py +++ b/app/webapi/routes/miniapp.py @@ -887,19 +887,6 @@ async def get_payment_methods( ) ) - if settings.is_freekassa_enabled(): - methods.append( - MiniAppPaymentMethod( - id="freekassa", - icon="💳", - requires_amount=True, - currency="RUB", - min_amount_kopeks=settings.FREEKASSA_MIN_AMOUNT_KOPEKS, - max_amount_kopeks=settings.FREEKASSA_MAX_AMOUNT_KOPEKS, - integration_type=MiniAppPaymentIntegrationType.REDIRECT, - ) - ) - if settings.TRIBUTE_ENABLED: methods.append( MiniAppPaymentMethod( @@ -916,14 +903,13 @@ async def get_payment_methods( "yookassa_sbp": 2, "yookassa": 3, "cloudpayments": 4, - "freekassa": 5, - "mulenpay": 6, - "pal24": 7, - "platega": 8, - "wata": 9, - "cryptobot": 10, - "heleket": 11, - "tribute": 12, + "mulenpay": 5, + "pal24": 6, + "platega": 7, + "wata": 8, + "cryptobot": 9, + "heleket": 10, + "tribute": 11, } methods.sort(key=lambda item: order_map.get(item.id, 99)) @@ -1397,47 +1383,6 @@ async def create_payment_link( }, ) - if method == "freekassa": - if not settings.is_freekassa_enabled(): - raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Payment method is unavailable") - if amount_kopeks is None or amount_kopeks <= 0: - raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Amount must be positive") - - if amount_kopeks < settings.FREEKASSA_MIN_AMOUNT_KOPEKS: - raise HTTPException( - status.HTTP_400_BAD_REQUEST, - detail=f"Amount is below minimum ({settings.FREEKASSA_MIN_AMOUNT_KOPEKS / 100:.2f} RUB)", - ) - if amount_kopeks > settings.FREEKASSA_MAX_AMOUNT_KOPEKS: - raise HTTPException( - status.HTTP_400_BAD_REQUEST, - detail=f"Amount exceeds maximum ({settings.FREEKASSA_MAX_AMOUNT_KOPEKS / 100:.2f} RUB)", - ) - - payment_service = PaymentService() - result = await payment_service.create_freekassa_payment( - db=db, - user_id=user.id, - amount_kopeks=amount_kopeks, - description=settings.get_balance_payment_description(amount_kopeks), - email=getattr(user, "email", None), - language=user.language or settings.DEFAULT_LANGUAGE, - ) - - if not result or not result.get("payment_url"): - raise HTTPException(status.HTTP_502_BAD_GATEWAY, detail="Failed to create payment") - - return MiniAppPaymentCreateResponse( - method=method, - payment_url=result["payment_url"], - amount_kopeks=amount_kopeks, - extra={ - "local_payment_id": result.get("local_payment_id"), - "order_id": result.get("order_id"), - "requested_at": _current_request_timestamp(), - }, - ) - if method == "tribute": if not settings.TRIBUTE_ENABLED: raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Payment method is unavailable") @@ -1536,8 +1481,6 @@ async def _resolve_payment_status_entry( return await _resolve_heleket_payment_status(db, user, query) if method == "cloudpayments": return await _resolve_cloudpayments_payment_status(db, user, query) - if method == "freekassa": - return await _resolve_freekassa_payment_status(db, user, query) if method == "stars": return await _resolve_stars_payment_status(db, user, query) if method == "tribute": @@ -2152,64 +2095,6 @@ async def _resolve_cloudpayments_payment_status( ) -async def _resolve_freekassa_payment_status( - db: AsyncSession, - user: User, - query: MiniAppPaymentStatusQuery, -) -> MiniAppPaymentStatusResult: - from app.database.crud.freekassa import ( - get_freekassa_payment_by_id, - get_freekassa_payment_by_order_id, - ) - - payment = None - if query.local_payment_id: - payment = await get_freekassa_payment_by_id(db, query.local_payment_id) - if not payment and query.payment_id: - payment = await get_freekassa_payment_by_order_id(db, query.payment_id) - - if not payment or payment.user_id != user.id: - return MiniAppPaymentStatusResult( - method="freekassa", - status="pending", - is_paid=False, - amount_kopeks=query.amount_kopeks, - message="Payment not found", - extra={ - "local_payment_id": query.local_payment_id, - "order_id": query.payment_id, - "payload": query.payload, - "started_at": query.started_at, - }, - ) - - status_raw = payment.status - is_paid = bool(payment.is_paid) - status = _classify_status(status_raw, is_paid) - completed_at = payment.paid_at or payment.updated_at or payment.created_at - - return MiniAppPaymentStatusResult( - method="freekassa", - status=status, - is_paid=status == "paid", - amount_kopeks=payment.amount_kopeks, - currency=payment.currency, - completed_at=completed_at, - transaction_id=payment.transaction_id, - external_id=payment.freekassa_order_id, - message=None, - extra={ - "status": payment.status, - "local_payment_id": payment.id, - "order_id": payment.order_id, - "freekassa_order_id": payment.freekassa_order_id, - "payment_url": payment.payment_url, - "payload": query.payload, - "started_at": query.started_at, - }, - ) - - async def _resolve_stars_payment_status( db: AsyncSession, user: User, diff --git a/miniapp/index.html b/miniapp/index.html index cc85c05f..11fdfe26 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -5736,8 +5736,6 @@ 'topup.method.tribute.description': 'Redirect to Tribute payment page', 'topup.method.cloudpayments.title': 'Bank card (CloudPayments)', 'topup.method.cloudpayments.description': 'Secure bank card payment', - 'topup.method.freekassa.title': 'Freekassa', - 'topup.method.freekassa.description': 'Various payment methods via Freekassa', 'topup.amount.title': 'Enter amount', 'topup.amount.subtitle': 'Specify how much you want to top up', 'topup.amount.placeholder': 'Amount in {currency}', @@ -6160,8 +6158,6 @@ 'topup.method.tribute.description': 'Переход на страницу оплаты Tribute', 'topup.method.cloudpayments.title': 'Банковская карта (CloudPayments)', 'topup.method.cloudpayments.description': 'Безопасная оплата банковской картой', - 'topup.method.freekassa.title': 'Freekassa', - 'topup.method.freekassa.description': 'Различные способы оплаты через Freekassa', 'topup.amount.title': 'Введите сумму', 'topup.amount.subtitle': 'Укажите сумму пополнения', 'topup.amount.placeholder': 'Сумма в {currency}',