From 8b2f9e1803d89231a3eaf0dde322c7750bb3d3db Mon Sep 17 00:00:00 2001 From: gy9vin Date: Fri, 17 Oct 2025 11:20:08 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=BF=D0=BB=D0=B0=D1=82=D0=B0=20=D0=BF?= =?UTF-8?q?=D0=BE=20qr=20=D1=81=20=D0=B4=D1=80=D1=83=D0=B3=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=20=D1=82=D0=B5=D0=BB=D0=B5=D1=84=D0=BE=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=BF=D0=BE=20=D0=A1=D0=91=D0=9F=20=D1=87=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B7=20=D0=AE=D0=9A=D0=B0=D1=81=D1=81=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/handlers/balance/yookassa.py | 134 ++++++++++++++++++++++++++----- app/services/payment/yookassa.py | 5 +- app/services/yookassa_service.py | 111 ++++++++++++++++++++++++- docker-compose.yml | 2 +- 4 files changed, 226 insertions(+), 26 deletions(-) diff --git a/app/handlers/balance/yookassa.py b/app/handlers/balance/yookassa.py index c7e9fe97..2b5601c7 100644 --- a/app/handlers/balance/yookassa.py +++ b/app/handlers/balance/yookassa.py @@ -246,34 +246,125 @@ async def process_yookassa_sbp_payment_amount( return confirmation_url = payment_result.get("confirmation_url") - if not confirmation_url: - await message.answer("❌ Ошибка получения ссылки для оплаты через СБП. Обратитесь в поддержку.") + qr_confirmation_data = payment_result.get("qr_confirmation_data") + + if not confirmation_url and not qr_confirmation_data: + await message.answer("❌ Ошибка получения данных для оплаты через СБП. Обратитесь в поддержку.") await state.clear() return - keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ - [types.InlineKeyboardButton(text="🏦 Оплатить через СБП", url=confirmation_url)], - [types.InlineKeyboardButton(text="📊 Проверить статус", callback_data=f"check_yookassa_{payment_result['local_payment_id']}")], - [types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")] - ]) + # Подготовим QR-код для вставки в основное сообщение + qr_photo = None + if qr_confirmation_data: + try: + # Импортируем необходимые модули для генерации QR-кода + import base64 + from io import BytesIO + import qrcode + from aiogram.types import BufferedInputFile + + # Создаем QR-код из полученных данных + qr = qrcode.QRCode(version=1, box_size=10, border=5) + qr.add_data(qr_confirmation_data) + qr.make(fit=True) + + img = qr.make_image(fill_color="black", back_color="white") + + # Сохраняем изображение в байты + img_bytes = BytesIO() + img.save(img_bytes, format='PNG') + img_bytes.seek(0) + + qr_photo = BufferedInputFile(img_bytes.getvalue(), filename="qrcode.png") + except ImportError: + logger.warning("qrcode библиотека не установлена, QR-код не будет сгенерирован") + except Exception as e: + logger.error(f"Ошибка генерации QR-кода: {e}") - await message.answer( - f"🏦 Оплата через СБП\n\n" + # Если нет QR-данных из YooKassa, но есть URL, генерируем QR-код из URL + if not qr_photo and confirmation_url: + try: + # Импортируем необходимые модули для генерации QR-кода + import base64 + from io import BytesIO + import qrcode + from aiogram.types import BufferedInputFile + + # Создаем QR-код из URL + qr = qrcode.QRCode(version=1, box_size=10, border=5) + qr.add_data(confirmation_url) + qr.make(fit=True) + + img = qr.make_image(fill_color="black", back_color="white") + + # Сохраняем изображение в байты + img_bytes = BytesIO() + img.save(img_bytes, format='PNG') + img_bytes.seek(0) + + qr_photo = BufferedInputFile(img_bytes.getvalue(), filename="qrcode.png") + except ImportError: + logger.warning("qrcode библиотека не установлена, QR-код не будет сгенерирован") + except Exception as e: + logger.error(f"Ошибка генерации QR-кода из URL: {e}") + + # Создаем клавиатуру с кнопками для оплаты по ссылке и проверки статуса + keyboard_buttons = [] + + # Добавляем кнопку оплаты, если доступна ссылка + if confirmation_url: + keyboard_buttons.append([types.InlineKeyboardButton(text="🔗 Перейти к оплате", url=confirmation_url)]) + else: + # Если ссылка недоступна, предлагаем оплатить через ID платежа в приложении банка + keyboard_buttons.append([types.InlineKeyboardButton(text="📱 Оплатить в приложении банка", callback_data="temp_disabled")]) + + # Добавляем общие кнопки + keyboard_buttons.append([types.InlineKeyboardButton(text="📊 Проверить статус", callback_data=f"check_yookassa_{payment_result['local_payment_id']}")]) + keyboard_buttons.append([types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")]) + + keyboard = types.InlineKeyboardMarkup(inline_keyboard=keyboard_buttons) + + # Подготавливаем текст сообщения + message_text = ( + f"🔗 Оплата через СБП\n\n" f"💰 Сумма: {settings.format_price(amount_kopeks)}\n" f"🆔 ID платежа: {payment_result['yookassa_payment_id'][:8]}...\n\n" - f"📱 Инструкция:\n" - f"1. Нажмите кнопку 'Оплатить через СБП'\n" - f"2. Вас перенаправит в приложение вашего банка\n" - f"3. Подтвердите платеж через СБП\n" - f"4. Деньги поступят на баланс автоматически\n\n" - f"🔒 Оплата происходит через защищенную систему YooKassa\n" - f"✅ Принимаем СБП от всех банков-участников\n\n" - f"❓ Если возникнут проблемы, обратитесь в {settings.get_support_contact_display_html()}", - reply_markup=keyboard, - parse_mode="HTML" ) - await state.clear() + # Добавляем инструкции в зависимости от доступных способов оплаты + if not confirmation_url: + message_text += ( + f"📱 Инструкция по оплате:\n" + f"1. Откройте приложение вашего банка\n" + f"2. Найдите функцию оплаты по реквизитам или перевод по СБП\n" + f"3. Введите ID платежа: {payment_result['yookassa_payment_id']}\n" + f"4. Подтвердите платеж в приложении банка\n" + f"5. Деньги поступят на баланс автоматически\n\n" + ) + + message_text += ( + f"🔒 Оплата происходит через защищенную систему YooKassa\n" + f"✅ Принимаем СБП от всех банков-участников\n\n" + f"❓ Если возникнут проблемы, обратитесь в {settings.get_support_contact_display_html()}" + ) + + # Отправляем сообщение с инструкциями и клавиатурой + # Если есть QR-код, отправляем его как медиа-сообщение + if qr_photo: + # Используем метод отправки медиа-группы или фото с описанием + await message.answer_photo( + photo=qr_photo, + caption=message_text, + reply_markup=keyboard, + parse_mode="HTML" + ) + else: + # Если QR-код недоступен, отправляем обычное текстовое сообщение + await message.answer( + message_text, + reply_markup=keyboard, + parse_mode="HTML" + ) logger.info(f"Создан платеж YooKassa СБП для пользователя {db_user.telegram_id}: " f"{amount_kopeks//100}₽, ID: {payment_result['yookassa_payment_id']}") @@ -284,6 +375,9 @@ async def process_yookassa_sbp_payment_amount( await state.clear() + + + @error_handler async def check_yookassa_payment_status( callback: types.CallbackQuery, diff --git a/app/services/payment/yookassa.py b/app/services/payment/yookassa.py index 65ccaca0..9bb8bd8f 100644 --- a/app/services/payment/yookassa.py +++ b/app/services/payment/yookassa.py @@ -172,7 +172,7 @@ class YooKassaPaymentMixin: currency="RUB", description=description, status=yookassa_response["status"], - confirmation_url=yookassa_response.get("confirmation_url"), + confirmation_url=yookassa_response.get("confirmation_url"), # Используем confirmation URL metadata_json=payment_metadata, payment_method_type="bank_card", yookassa_created_at=None, @@ -193,7 +193,8 @@ class YooKassaPaymentMixin: return { "local_payment_id": local_payment.id, "yookassa_payment_id": yookassa_response["id"], - "confirmation_url": yookassa_response.get("confirmation_url"), + "confirmation_url": yookassa_response.get("confirmation_url"), # URL для подтверждения + "qr_confirmation_data": yookassa_response.get("qr_confirmation_data"), # Данные для QR-кода "confirmation_token": confirmation_token, "amount_kopeks": amount_kopeks, "amount_rubles": amount_rubles, diff --git a/app/services/yookassa_service.py b/app/services/yookassa_service.py index baf001bd..3a451014 100644 --- a/app/services/yookassa_service.py +++ b/app/services/yookassa_service.py @@ -188,6 +188,8 @@ class YooKassaService: } try: + # Создаем один платеж с подтверждением через QR + # Это позволит получить QR-код для пользователя builder = PaymentRequestBuilder() builder.set_amount({ @@ -197,6 +199,7 @@ class YooKassaService: builder.set_capture(True) + # Устанавливаем подтверждение через redirect для получения вебхуков builder.set_confirmation({ "type": "redirect", "return_url": self.return_url @@ -234,7 +237,7 @@ class YooKassaService: payment_request = builder.build() logger.info( - f"Создание платежа YooKassa СБП (Idempotence-Key: {idempotence_key}). " + f"Создание платежа YooKassa СБП с подтверждением 'qr' (Idempotence-Key: {idempotence_key}). " f"Сумма: {amount} {currency}. Метаданные: {metadata}. Чек: {receipt_data_dict}") loop = asyncio.get_running_loop() @@ -242,11 +245,14 @@ class YooKassaService: None, lambda: YooKassaPayment.create(payment_request, idempotence_key)) logger.info( - f"Ответ YooKassa Payment.create (СБП): ID={response.id}, Status={response.status}, Paid={response.paid}") + f"Ответ YooKassa Payment.create (СБП, qr): ID={response.id}, Status={response.status}, Paid={response.paid}") + # Возвращаем данные платежа с QR-подтверждением + # Пользователь может использовать QR-код или оплатить через приложение банка по ID платежа return { "id": response.id, - "confirmation_url": response.confirmation.confirmation_url if response.confirmation else None, + "qr_confirmation_data": response.confirmation.confirmation_data if response.confirmation and hasattr(response.confirmation, 'confirmation_data') else None, + "confirmation_url": response.confirmation.confirmation_url if response.confirmation and hasattr(response.confirmation, 'confirmation_url') else None, "status": response.status, "metadata": response.metadata, "amount_value": float(response.amount.value), @@ -263,6 +269,105 @@ class YooKassaService: logger.error(f"Ошибка создания платежа YooKassa СБП: {e}", exc_info=True) return None + async def _create_sbp_payment_with_confirmation_type( + self, + amount: float, + currency: str, + description: str, + metadata: Dict[str, Any], + customer_contact_for_receipt: Dict[str, str], + confirmation_type: str) -> Optional[Dict[str, Any]]: + """Создает SBP платеж с указанным типом подтверждения""" + try: + builder = PaymentRequestBuilder() + + builder.set_amount({ + "value": str(round(amount, 2)), + "currency": currency.upper() + }) + + builder.set_capture(True) + + if confirmation_type == "qr": + builder.set_confirmation({ + "type": "qr" + }) + else: # redirect + builder.set_confirmation({ + "type": "redirect", + "return_url": self.return_url + }) + + builder.set_description(description) + + builder.set_metadata(metadata) + + builder.set_payment_method_data({ + "type": "sbp" + }) + + receipt_items_list: List[Dict[str, Any]] = [{ + "description": description[:128], + "quantity": "1.00", + "amount": { + "value": str(round(amount, 2)), + "currency": currency.upper() + }, + "vat_code": str(getattr(settings, 'YOOKASSA_VAT_CODE', 1)), + "payment_mode": getattr(settings, 'YOOKASSA_PAYMENT_MODE', 'full_payment'), + "payment_subject": getattr(settings, 'YOOKASSA_PAYMENT_SUBJECT', 'service') + }] + + receipt_data_dict: Dict[str, Any] = { + "customer": customer_contact_for_receipt, + "items": receipt_items_list + } + + builder.set_receipt(receipt_data_dict) + + idempotence_key = str(uuid.uuid4()) + + payment_request = builder.build() + + logger.info( + f"Создание платежа YooKassa СБП с подтверждением '{confirmation_type}' (Idempotence-Key: {idempotence_key}). " + f"Сумма: {amount} {currency}. Метаданные: {metadata}. Чек: {receipt_data_dict}") + + loop = asyncio.get_running_loop() + response = await loop.run_in_executor( + None, lambda: YooKassaPayment.create(payment_request, idempotence_key)) + + logger.info( + f"Ответ YooKassa Payment.create (СБП, {confirmation_type}): ID={response.id}, Status={response.status}, Paid={response.paid}") + + result = { + "id": response.id, + "status": response.status, + "metadata": response.metadata, + "amount_value": float(response.amount.value), + "amount_currency": response.amount.currency, + "idempotence_key_used": idempotence_key, + "paid": response.paid, + "refundable": response.refundable, + "created_at": response.created_at.isoformat() if hasattr( + response.created_at, 'isoformat') else str(response.created_at), + "description_from_yk": response.description, + "test_mode": response.test if hasattr(response, 'test') else None + } + + # Добавляем данные подтверждения в зависимости от типа + if confirmation_type == "qr": + if response.confirmation and hasattr(response.confirmation, 'confirmation_data'): + result["confirmation_data"] = response.confirmation.confirmation_data + else: # redirect + if response.confirmation and hasattr(response.confirmation, 'confirmation_url'): + result["confirmation_url"] = response.confirmation.confirmation_url + + return result + except Exception as e: + logger.error(f"Ошибка создания платежа YooKassa СБП с подтверждением '{confirmation_type}': {e}", exc_info=True) + return None + async def get_payment_info( self, payment_id_in_yookassa: str) -> Optional[Dict[str, Any]]: diff --git a/docker-compose.yml b/docker-compose.yml index f3d58b05..d7849b7a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,7 +35,7 @@ services: retries: 3 bot: - image: fr1ngg/remnawave-bedolaga-telegram-bot:latest + build: . container_name: remnawave_bot restart: unless-stopped depends_on: