diff --git a/bot/app/web/web_server.py b/bot/app/web/web_server.py index 5da6d7f..49783f9 100644 --- a/bot/app/web/web_server.py +++ b/bot/app/web/web_server.py @@ -41,6 +41,7 @@ async def build_and_start_web_app( setup_application(app, dp, bot=bot) telegram_uses_webhook_mode = bool(settings.WEBHOOK_BASE_URL) + telegram_webhook_secret = (settings.TELEGRAM_WEBHOOK_SECRET or "").strip() or None if telegram_uses_webhook_mode: telegram_webhook_path = settings.telegram_webhook_path @@ -49,13 +50,13 @@ async def build_and_start_web_app( SimpleRequestHandler( dispatcher=dp, bot=bot, - secret_token=settings.TELEGRAM_WEBHOOK_SECRET, + secret_token=telegram_webhook_secret, ), ) logging.info( "Telegram webhook route configured at: [POST] %s (secret_token=%s)", telegram_webhook_path, - "set" if settings.TELEGRAM_WEBHOOK_SECRET else "not_set", + "set" if telegram_webhook_secret else "not_set", ) from bot.handlers.user.payment import yookassa_webhook_route diff --git a/bot/handlers/user/payment.py b/bot/handlers/user/payment.py index f60258c..c9f0e1c 100644 --- a/bot/handlers/user/payment.py +++ b/bot/handlers/user/payment.py @@ -51,12 +51,10 @@ async def process_successful_payment(session: AsyncSession, bot: Bot, auto_renew_subscription_id_str = metadata.get( "auto_renew_for_subscription_id") - # For auto-renew payments, payment_db_id may be absent. In that case, - # we will create/ensure a payment record idempotently using provider payment id. if ( not user_id_str or (not subscription_months_str and not traffic_gb_str) - or (not payment_db_id_str and not auto_renew_subscription_id_str) + or not payment_db_id_str ): logging.error( f"Missing crucial metadata for payment: {payment_info_from_webhook.get('id')}, metadata: {metadata}" @@ -68,57 +66,37 @@ async def process_successful_payment(session: AsyncSession, bot: Bot, user_id = int(user_id_str) subscription_months = float(subscription_months_str or 0) traffic_amount_gb = float(traffic_gb_str) if traffic_gb_str else subscription_months - payment_db_id = int( - payment_db_id_str) if payment_db_id_str and payment_db_id_str.isdigit() else None - is_auto_renew = bool(auto_renew_subscription_id_str and not payment_db_id and sale_mode != "traffic") + if not payment_db_id_str.isdigit(): + logging.error( + "Invalid payment_db_id metadata for payment %s: %s", + payment_info_from_webhook.get("id"), + payment_db_id_str, + ) + return + payment_db_id = int(payment_db_id_str) + is_auto_renew = bool(auto_renew_subscription_id_str and sale_mode != "traffic") promo_code_id = int( promo_code_id_str ) if promo_code_id_str and promo_code_id_str.isdigit() else None amount_data = payment_info_from_webhook.get("amount", {}) - months_for_record = int(subscription_months) if sale_mode != "traffic" else 0 payment_value = float(amount_data.get("value", 0.0)) yk_payment_id_from_hook = payment_info_from_webhook.get("id") - payment_record = None - # If this is an auto-renewal (no payment_db_id in metadata), ensure a payment record exists - if payment_db_id is None and auto_renew_subscription_id_str: - try: - if not yk_payment_id_from_hook: - logging.error( - "Auto-renew webhook missing YooKassa payment id; cannot ensure payment record." - ) - return - from db.dal import payment_dal as _payment_dal - payment_record = await _payment_dal.get_payment_by_provider_payment_id( - session, yk_payment_id_from_hook - ) - if not payment_record: - payment_record = await _payment_dal.ensure_payment_with_provider_id( - session, - user_id=user_id, - amount=payment_value, - currency=amount_data.get("currency", "RUB"), - months=months_for_record or 1, - description=payment_info_from_webhook.get( - "description") or f"Auto-renewal for {months_for_record or subscription_months} months", - provider="yookassa", - provider_payment_id=yk_payment_id_from_hook, - ) - payment_db_id = payment_record.payment_id - except Exception as e_ensure: - logging.error( - f"Failed to ensure payment record for auto-renew webhook (YK {payment_info_from_webhook.get('id')}): {e_ensure}", - exc_info=True, - ) - return - elif payment_db_id is not None: - payment_record = await payment_dal.get_payment_by_db_id(session, payment_db_id) - if not payment_record: - logging.error( - f"Payment record {payment_db_id} not found for YK ID {yk_payment_id_from_hook}." - ) - return + payment_record = await payment_dal.get_payment_by_db_id(session, payment_db_id) + if not payment_record: + logging.error( + f"Payment record {payment_db_id} not found for YK ID {yk_payment_id_from_hook}." + ) + return + if payment_record.user_id != user_id: + logging.error( + "Payment ownership mismatch for payment %s: metadata user_id=%s, db user_id=%s", + payment_db_id, + user_id, + payment_record.user_id, + ) + return # Provider-backed verification (defense-in-depth): verify actual YooKassa payment state if yk_payment_id_from_hook and yookassa_service and yookassa_service.configured: @@ -151,7 +129,8 @@ async def process_successful_payment(session: AsyncSession, bot: Bot, user_id, ) return - if payment_db_id and str(provider_metadata.get("payment_db_id") or "") != str(payment_db_id): + provider_payment_db_id = str(provider_metadata.get("payment_db_id") or "").strip() + if provider_payment_db_id != str(payment_db_id): logging.error( "YooKassa webhook verification failed: payment_db_id mismatch for payment %s (provider=%s, expected=%s)", yk_payment_id_from_hook, @@ -237,6 +216,13 @@ async def process_successful_payment(session: AsyncSession, bot: Bot, session, payment_db_id, ) + if payment_before_update and payment_before_update.status == "succeeded": + logging.info( + "YooKassa webhook ignored: payment %s already succeeded (db_id=%s)", + yk_payment_id_from_hook, + payment_db_id, + ) + return should_send_lknpd_receipt = bool( lknpd_service and lknpd_service.configured diff --git a/bot/main_bot.py b/bot/main_bot.py index 93dcdda..d9a1806 100644 --- a/bot/main_bot.py +++ b/bot/main_bot.py @@ -77,11 +77,12 @@ async def on_startup_configured(dispatcher: Dispatcher): else: logging.info("STARTUP: Telegram webhook currently empty (will set).") + telegram_webhook_secret = (settings.TELEGRAM_WEBHOOK_SECRET or "").strip() or None set_success = await bot.set_webhook( url=full_telegram_webhook_url, drop_pending_updates=True, allowed_updates=dispatcher.resolve_used_update_types(), - secret_token=settings.TELEGRAM_WEBHOOK_SECRET, + secret_token=telegram_webhook_secret, ) if set_success: logging.info("STARTUP: bot.set_webhook returned SUCCESS (True).") diff --git a/bot/services/subscription_service.py b/bot/services/subscription_service.py index 719516c..d630141 100644 --- a/bot/services/subscription_service.py +++ b/bot/services/subscription_service.py @@ -5,7 +5,7 @@ from typing import Optional, Dict, Any, List, Tuple from aiogram import Bot from bot.middlewares.i18n import JsonI18n -from db.dal import user_dal, subscription_dal, promo_code_dal, user_billing_dal +from db.dal import user_dal, subscription_dal, promo_code_dal, user_billing_dal, payment_dal from bot.utils.date_utils import add_months from bot.utils.config_link import prepare_config_links from db.models import User, Subscription @@ -1007,15 +1007,30 @@ class SubscriptionService: logging.error(f"Auto-renew price missing for {months} months") return False + payment_description = f"Auto-renewal for {months} months" + payment_record = await payment_dal.create_payment_record( + session, + { + "user_id": sub.user_id, + "amount": float(amount), + "currency": "RUB", + "status": "pending_yookassa", + "description": payment_description, + "subscription_duration_months": int(months), + "provider": "yookassa", + }, + ) + metadata = { "user_id": str(sub.user_id), "auto_renew_for_subscription_id": str(sub.subscription_id), "subscription_months": str(months), + "payment_db_id": str(payment_record.payment_id), } resp = await yk.create_payment( amount=float(amount), currency="RUB", - description=f"Auto-renewal for {months} months", + description=payment_description, metadata=metadata, payment_method_id=default_pm.provider_payment_method_id, save_payment_method=False, @@ -1024,6 +1039,14 @@ class SubscriptionService: if not resp or resp.get("status") not in {"pending", "waiting_for_capture", "succeeded"}: logging.error(f"Auto-renew create_payment failed: {resp}") return False + provider_payment_id = resp.get("id") + if provider_payment_id: + await payment_dal.update_provider_payment_and_status( + session, + payment_db_id=payment_record.payment_id, + provider_payment_id=provider_payment_id, + new_status="pending_yookassa", + ) logging.info(f"Auto-renew initiated for user {sub.user_id} payment_id={resp.get('id')}") return True