mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-24 21:30:52 +00:00
Revert "Frekassa"
This commit is contained in:
17
.env.example
17
.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 - только текст)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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())
|
||||
@@ -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 (
|
||||
"<FreekassaPayment(id={0}, order_id={1}, amount={2}₽, status={3})>".format(
|
||||
self.id,
|
||||
self.order_id,
|
||||
self.amount_rubles,
|
||||
self.status,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class PromoGroup(Base):
|
||||
__tablename__ = "promo_groups"
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
87
app/external/webhook_server.py
vendored
87
app/external/webhook_server.py
vendored
@@ -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)
|
||||
|
||||
@@ -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 = (
|
||||
"🧪 <b>Тестовый платеж Freekassa</b>\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()
|
||||
|
||||
|
||||
@@ -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",
|
||||
"💳 <b>Оплата через {name}</b>\n\n"
|
||||
"Сумма: <b>{amount}₽</b>\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"🚫 <b>Пополнение ограничено</b>\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"🚫 <b>Пополнение ограничено</b>\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",
|
||||
"💳 <b>Пополнение через {name}</b>\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"🚫 <b>Пополнение ограничено</b>\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,
|
||||
)
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
(
|
||||
"✅ <b>Пополнение успешно!</b>\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,
|
||||
}
|
||||
@@ -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-ам."""
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}',
|
||||
|
||||
Reference in New Issue
Block a user