From b999e4acedd9abcd3b8f3fd1665d1d6aa998cda1 Mon Sep 17 00:00:00 2001 From: Vladless Date: Sun, 10 Aug 2025 23:06:26 +0300 Subject: [PATCH] robokassa nomenclature/ minor fixes for API --- api/routes/gifts.py | 21 +++++++++-- api/schemas/misc.py | 2 +- database/importer.py | 6 ++-- database/models.py | 6 +++- handlers/payments/robokassa_pay.py | 56 +++++++++++++++++++++--------- 5 files changed, 68 insertions(+), 23 deletions(-) diff --git a/api/routes/gifts.py b/api/routes/gifts.py index 3db47d77..2c155218 100644 --- a/api/routes/gifts.py +++ b/api/routes/gifts.py @@ -1,5 +1,5 @@ from fastapi import APIRouter, Depends, HTTPException, Path -from sqlalchemy import select +from sqlalchemy import delete, select from sqlalchemy.ext.asyncio import AsyncSession from api.depends import get_session, verify_admin_token @@ -18,7 +18,7 @@ gift_router = generate_crud_router( schema_update=GiftUpdate, identifier_field="gift_id", parameter_name="gift_id", - enabled_methods=["get_all", "get_one", "create", "update", "delete"], + enabled_methods=["get_all", "get_one", "create", "update"], ) router.include_router(gift_router, prefix="", tags=["Gifts"]) @@ -46,3 +46,20 @@ gift_usage_router = generate_crud_router( ) router.include_router(gift_usage_router, prefix="/usages", tags=["GiftUsages"]) router.include_router(gift_usage_router, prefix="/usages", tags=["Gifts"]) + + +@router.delete("/{gift_id}", response_model=dict, tags=["Gifts"]) +async def delete_gift_with_usages( + gift_id: str = Path(..., description="ID подарка"), + admin: Admin = Depends(verify_admin_token), + session: AsyncSession = Depends(get_session), +): + result = await session.execute(select(Gift).where(Gift.gift_id == gift_id)) + gift = result.scalar_one_or_none() + if not gift: + raise HTTPException(status_code=404, detail="Gift not found") + + await session.execute(delete(GiftUsage).where(GiftUsage.gift_id == gift_id)) + await session.delete(gift) + await session.commit() + return {"message": "Подарок и связанные использования удалены"} diff --git a/api/schemas/misc.py b/api/schemas/misc.py index f24f716c..d97bed65 100644 --- a/api/schemas/misc.py +++ b/api/schemas/misc.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import Literal, Optional, Union +from typing import Literal from pydantic import BaseModel diff --git a/database/importer.py b/database/importer.py index 396e224f..a505acd9 100644 --- a/database/importer.py +++ b/database/importer.py @@ -18,11 +18,13 @@ async def import_keys_from_3xui_db(db_path: str, session: AsyncSession) -> tuple skipped = 0 if USE_COUNTRY_SELECTION: - result = await session.execute(select(Server.name).where(Server.enabled is True, Server.panel_type == "3x-ui")) + result = await session.execute( + select(Server.server_name).where(Server.enabled.is_(True), Server.panel_type == "3x-ui") + ) else: result = await session.execute( select(Server.cluster_name) - .where(Server.enabled is True, Server.panel_type == "3x-ui", Server.cluster_name.isnot(None)) + .where(Server.enabled.is_(True), Server.panel_type == "3x-ui", Server.cluster_name.isnot(None)) .distinct() ) diff --git a/database/models.py b/database/models.py index f66f8bfa..d7d7da96 100644 --- a/database/models.py +++ b/database/models.py @@ -123,7 +123,11 @@ class Coupon(DictLikeMixin, Base): class CouponUsage(DictLikeMixin, Base): __tablename__ = "coupon_usages" - coupon_id = Column(Integer, ForeignKey("coupons.id"), primary_key=True) + coupon_id = Column( + Integer, + ForeignKey("coupons.id", ondelete="CASCADE"), # Каскадное удаление + primary_key=True + ) user_id = Column(BigInteger, primary_key=True) used_at = Column(DateTime, default=datetime.utcnow) diff --git a/handlers/payments/robokassa_pay.py b/handlers/payments/robokassa_pay.py index 7d90f284..725a1201 100644 --- a/handlers/payments/robokassa_pay.py +++ b/handlers/payments/robokassa_pay.py @@ -12,6 +12,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from datetime import datetime, timedelta from sqlalchemy import select, and_ from pytz import timezone +from typing import Optional from config import ( ROBOKASSA_ENABLE, @@ -58,16 +59,35 @@ if ROBOKASSA_ENABLE: logger.info("Robokassa initialized with login: {}", ROBOKASSA_LOGIN) +def _build_receipt(amount: float, *, sno: Optional[str] = None) -> dict: + receipt = { + "items": [{ + "name": "Пополнение баланса", + "quantity": 1, + "sum": float(amount), + "payment_method": "full_payment", + "payment_object": "payment", + "tax": "none", + }] + } + if sno: + receipt["sno"] = sno + return receipt + + def generate_payment_link(amount, inv_id, description, tg_id): - """Генерация ссылки на оплату.""" + """Генерация ссылки на оплату с номенклатурой (Receipt).""" logger.debug( f"Generating payment link for amount: {amount}, inv_id: {inv_id}, description: {description}" ) + receipt = _build_receipt(amount) + payment_link = robokassa._payment.link.generate_by_script( - out_sum=amount, + out_sum=float(amount), inv_id=inv_id, description=f"Пополнение баланса (tg_id: {tg_id})", - id=f"{tg_id}", + id=str(tg_id), + receipt=receipt ) logger.info(f"Generated payment link: {payment_link}") return payment_link @@ -247,24 +267,26 @@ async def robokassa_webhook(request: web.Request): def check_payment_signature(params): - """Проверка подписи запроса от Robokassa с учетом shp_id.""" - out_sum = params.get("OutSum") - inv_id = params.get("InvId") - signature_value = params.get("SignatureValue") - shp_id = params.get("shp_id") + """Проверка подписи ResultURL от Robokassa с учётом всех Shp_*.""" + out_sum = params.get("OutSum") or params.get("out_summ") or params.get("outsumm") + inv_id = params.get("InvId") or params.get("inv_id") or params.get("invid") + received_sig = (params.get("SignatureValue") or params.get("signaturevalue") or "").upper() - signature_string = f"{out_sum}:{inv_id}:{ROBOKASSA_PASSWORD2}:shp_id={shp_id}" + if not out_sum or not inv_id or not received_sig: + logger.error("Missing required params for signature check.") + return False - logger.info(f"Signature string before hashing: {signature_string}") + shp_items = [(k, params[k]) for k in params.keys() if k.lower().startswith("shp_")] + shp_items.sort(key=lambda kv: kv[0].lower()) + shp_suffix = "".join(f":{k}={v}" for k, v in shp_items) + base = f"{out_sum}:{inv_id}:{ROBOKASSA_PASSWORD2}{shp_suffix}" + expected_sig = hashlib.md5(base.encode("utf-8")).hexdigest().upper() - expected_signature = ( - hashlib.md5(signature_string.encode("utf-8")).hexdigest().upper() - ) + logger.info(f"Signature base (RESULT): {base}") + logger.info(f"Expected signature: {expected_sig}") + logger.info(f"Received signature: {received_sig}") - logger.info(f"Expected signature: {expected_signature}") - logger.info(f"Received signature: {signature_value}") - - return signature_value.upper() == expected_signature.upper() + return received_sig == expected_sig @router.callback_query(F.data == "enter_custom_amount_robokassa")