Merge branch 'Fr1ngg:main' into DEV

This commit is contained in:
Legacyyy777
2025-09-17 18:14:03 +05:00
committed by GitHub
10 changed files with 268 additions and 165 deletions

View File

@@ -1,38 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -1,10 +0,0 @@
---
name: Custom issue template
about: Describe this issue template's purpose here.
title: ''
labels: ''
assignees: ''
---

View File

@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

111
.github/workflows/docker-hub.yml vendored Normal file
View File

@@ -0,0 +1,111 @@
name: BedolagaBot
on:
push:
branches:
- main
- dev
tags:
- 'v*'
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Get version info
id: version
run: |
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
echo "build_date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
# Определяем версию и теги
if [[ $GITHUB_REF == refs/tags/* ]]; then
VERSION=${GITHUB_REF#refs/tags/}
TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:latest,fr1ngg/remnawave-bedolaga-telegram-bot:${VERSION}"
echo "🏷️ Собираем релизную версию: $VERSION"
elif [[ $GITHUB_REF == refs/heads/main ]]; then
VERSION="v2.3.4-$(git rev-parse --short HEAD)"
TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:latest,fr1ngg/remnawave-bedolaga-telegram-bot:${VERSION}"
echo "🚀 Собираем версию из main: $VERSION"
elif [[ $GITHUB_REF == refs/heads/dev ]]; then
VERSION="v2.3.4-dev-$(git rev-parse --short HEAD)"
TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:dev,fr1ngg/remnawave-bedolaga-telegram-bot:${VERSION}"
echo "🧪 Собираем dev версию: $VERSION"
else
VERSION="v2.3.4-pr-$(git rev-parse --short HEAD)"
TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:pr-$(git rev-parse --short HEAD)"
echo "🔀 Собираем PR версию: $VERSION"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "tags=$TAGS" >> $GITHUB_OUTPUT
echo "should_push=${{ github.event_name != 'pull_request' }}" >> $GITHUB_OUTPUT
echo "=== Информация о сборке ==="
echo "Версия: $VERSION"
echo "Коммит: $(git rev-parse --short HEAD)"
echo "Теги: $TAGS"
echo "Push: ${{ github.event_name != 'pull_request' }}"
echo "==========================="
- name: Login to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: ${{ steps.version.outputs.should_push }}
tags: ${{ steps.version.outputs.tags }}
build-args: |
VERSION=${{ steps.version.outputs.version }}
BUILD_DATE=${{ steps.version.outputs.build_date }}
VCS_REF=${{ steps.version.outputs.short_sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
labels: |
org.opencontainers.image.version=${{ steps.version.outputs.version }}
org.opencontainers.image.created=${{ steps.version.outputs.build_date }}
org.opencontainers.image.revision=${{ steps.version.outputs.short_sha }}
org.opencontainers.image.title=Bedolaga RemnaWave Bot
org.opencontainers.image.description=Telegram bot for RemnaWave VPN service
org.opencontainers.image.source=https://github.com/${{ github.repository }}
- name: Build Summary
if: github.event_name != 'pull_request'
run: |
echo "## 🚀 Docker Hub Build Summary" >> $GITHUB_STEP_SUMMARY
echo "| Параметр | Значение |" >> $GITHUB_STEP_SUMMARY
echo "|----------|----------|" >> $GITHUB_STEP_SUMMARY
echo "| **Версия** | \`${{ steps.version.outputs.version }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| **Коммит** | \`${{ steps.version.outputs.short_sha }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| **Дата сборки** | \`${{ steps.version.outputs.build_date }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| **Registry** | Docker Hub |" >> $GITHUB_STEP_SUMMARY
echo "| **Ветка** | \`${{ github.ref_name }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| **Статус** | ✅ Опубликован |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### 📋 Опубликованные теги:" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
echo "${{ steps.version.outputs.tags }}" | tr ',' '\n' >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### 🔗 Ссылки:" >> $GITHUB_STEP_SUMMARY
echo "- [Docker Hub Repository](https://hub.docker.com/r/fr1ngg/remnawave-bedolaga-telegram-bot)" >> $GITHUB_STEP_SUMMARY

154
.github/workflows/docker-registry.yml vendored Normal file
View File

@@ -0,0 +1,154 @@
name: Build and Publish Docker Image
on:
push:
branches:
- main
- dev
tags:
- 'v*'
pull_request:
branches:
- main
- dev
env:
REGISTRY: ghcr.io
IMAGE_NAME: fr1ngg/remnawave-bedolaga-telegram-bot
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: |
network=host
- name: Log in to Container Registry
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get version info
id: version
run: |
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
echo "build_date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
if [[ $GITHUB_REF == refs/tags/* ]]; then
VERSION=${GITHUB_REF#refs/tags/}
echo "🏷️ Собираем релизную версию: $VERSION"
elif [[ $GITHUB_REF == refs/heads/main ]]; then
VERSION="v2.3.4"
echo "🚀 Собираем версию из main: $VERSION"
elif [[ $GITHUB_REF == refs/heads/dev ]]; then
VERSION="v2.3.4-dev-$(git rev-parse --short HEAD)"
echo "🧪 Собираем dev версию: $VERSION"
else
VERSION="v2.3.4-pr-$(git rev-parse --short HEAD)"
echo "🔀 Собираем PR версию: $VERSION"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
# Определяем, нужно ли пушить образ
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
if [[ "${{ github.event.pull_request.head.repo.full_name }}" == "${{ github.repository }}" ]]; then
echo "should_push=true" >> $GITHUB_OUTPUT
echo "✅ PR из того же репозитория - будем пушить"
else
echo "should_push=false" >> $GITHUB_OUTPUT
echo "⚠️ PR из внешнего форка - только build без push"
fi
else
echo "should_push=true" >> $GITHUB_OUTPUT
echo "✅ Push/Tag - будем пушить"
fi
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable={{is_default_branch}}
type=raw,value=dev,enable=${{ github.ref == 'refs/heads/dev' }}
type=raw,value=${{ steps.version.outputs.version }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: linux/amd64
push: ${{ steps.version.outputs.should_push }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
VERSION=${{ steps.version.outputs.version }}
BUILD_DATE=${{ steps.version.outputs.build_date }}
VCS_REF=${{ steps.version.outputs.short_sha }}
cache-from: |
type=gha
type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
cache-to: |
type=gha,mode=max
type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max
build-contexts: |
alpine=docker-image://alpine:latest
- name: Generate security report
uses: docker/scout-action@v1
if: github.event_name == 'pull_request' && steps.version.outputs.should_push == 'true'
with:
command: quickview,compare
image: ${{ steps.meta.outputs.tags }}
to: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
ignore-unchanged: true
only-severities: critical,high
write-comment: true
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Build Summary
if: steps.version.outputs.should_push == 'true'
run: |
echo "## 🚀 Docker Build Summary" >> $GITHUB_STEP_SUMMARY
echo "| Параметр | Значение |" >> $GITHUB_STEP_SUMMARY
echo "|----------|----------|" >> $GITHUB_STEP_SUMMARY
echo "| **Версия** | \`${{ steps.version.outputs.version }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| **Коммит** | \`${{ steps.version.outputs.short_sha }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| **Дата сборки** | \`${{ steps.version.outputs.build_date }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| **Registry** | \`${{ env.REGISTRY }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| **Образ** | \`${{ env.IMAGE_NAME }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| **Ветка** | \`${{ github.ref_name }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| **Статус** | ✅ Опубликован |" >> $GITHUB_STEP_SUMMARY
- name: Build Summary (No Push)
if: steps.version.outputs.should_push == 'false'
run: |
echo "## 🔨 Docker Build Summary (Test Only)" >> $GITHUB_STEP_SUMMARY
echo "| Параметр | Значение |" >> $GITHUB_STEP_SUMMARY
echo "|----------|----------|" >> $GITHUB_STEP_SUMMARY
echo "| **Версия** | \`${{ steps.version.outputs.version }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| **Коммит** | \`${{ steps.version.outputs.short_sha }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| **Дата сборки** | \`${{ steps.version.outputs.build_date }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| **Статус** | ✅ Собран успешно (без публикации) |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "⚠️ **Примечание:** Образ собран но не опубликован, так как это PR из внешнего форка." >> $GITHUB_STEP_SUMMARY

View File

@@ -62,7 +62,6 @@ class Settings(BaseSettings):
NOTIFICATION_CACHE_HOURS: int = 24
BASE_SUBSCRIPTION_PRICE: int = 50000
AVAILABLE_SUBSCRIPTION_PERIODS: str = "14,30,60,90,180,360"
AVAILABLE_RENEWAL_PERIODS: str = "30,90,180"
PRICE_14_DAYS: int = 50000

View File

@@ -210,7 +210,6 @@ async def start_yookassa_payment(
await callback.answer("❌ Оплата картой через YooKassa временно недоступна", show_alert=True)
return
# Получаем лимиты из настроек
min_amount_rub = settings.YOOKASSA_MIN_AMOUNT_KOPEKS / 100
max_amount_rub = settings.YOOKASSA_MAX_AMOUNT_KOPEKS / 100
@@ -234,12 +233,10 @@ async def start_yookassa_sbp_payment(
):
texts = get_texts(db_user.language)
# Проверяем, включена ли оплата через СБП
if not settings.is_yookassa_enabled() or not settings.YOOKASSA_SBP_ENABLED:
await callback.answer("❌ Оплата через СБП временно недоступна", show_alert=True)
return
# Получаем лимиты из настроек
min_amount_rub = settings.YOOKASSA_MIN_AMOUNT_KOPEKS / 100
max_amount_rub = settings.YOOKASSA_MAX_AMOUNT_KOPEKS / 100
@@ -357,7 +354,6 @@ async def process_topup_amount(
try:
amount_rubles = float(message.text.replace(',', '.'))
# Проверяем общие лимиты
if amount_rubles < 1:
await message.answer("Минимальная сумма пополнения: 1 ₽")
return
@@ -370,7 +366,6 @@ async def process_topup_amount(
data = await state.get_data()
payment_method = data.get("payment_method", "stars")
# Проверяем лимиты для YooKassa (если выбран этот метод)
if payment_method in ["yookassa", "yookassa_sbp"]:
if amount_kopeks < settings.YOOKASSA_MIN_AMOUNT_KOPEKS:
min_rubles = settings.YOOKASSA_MIN_AMOUNT_KOPEKS / 100
@@ -388,7 +383,7 @@ async def process_topup_amount(
from app.database.database import AsyncSessionLocal
async with AsyncSessionLocal() as db:
await process_yookassa_payment_amount(message, db_user, db, amount_kopeks, state)
elif payment_method == "yookassa_sbp": # Обработка оплаты через СБП
elif payment_method == "yookassa_sbp":
from app.database.database import AsyncSessionLocal
async with AsyncSessionLocal() as db:
await process_yookassa_sbp_payment_amount(message, db_user, db, amount_kopeks, state)
@@ -469,7 +464,6 @@ async def process_yookassa_payment_amount(
await message.answer("❌ Оплата через YooKassa временно недоступна")
return
# Проверяем лимиты из настроек
if amount_kopeks < settings.YOOKASSA_MIN_AMOUNT_KOPEKS:
min_rubles = settings.YOOKASSA_MIN_AMOUNT_KOPEKS / 100
await message.answer(f"❌ Минимальная сумма для оплаты картой: {min_rubles:.0f}")
@@ -549,17 +543,12 @@ async def process_yookassa_sbp_payment_amount(
amount_kopeks: int,
state: FSMContext
):
"""
Обработчик оплаты через СБП (Систему быстрых платежей) с использованием YooKassa
"""
texts = get_texts(db_user.language)
# Проверяем, включена ли оплата через СБП
if not settings.is_yookassa_enabled() or not settings.YOOKASSA_SBP_ENABLED:
await message.answer("❌ Оплата через СБП временно недоступна")
return
# Проверяем лимиты из настроек
if amount_kopeks < settings.YOOKASSA_MIN_AMOUNT_KOPEKS:
min_rubles = settings.YOOKASSA_MIN_AMOUNT_KOPEKS / 100
await message.answer(f"❌ Минимальная сумма для оплаты через СБП: {min_rubles:.0f}")
@@ -571,10 +560,8 @@ async def process_yookassa_sbp_payment_amount(
return
try:
# Создаем платеж через PaymentService
payment_service = PaymentService(message.bot)
# Создаем платеж с указанием метода оплаты СБП
payment_result = await payment_service.create_yookassa_sbp_payment(
db=db,
user_id=db_user.id,
@@ -589,27 +576,23 @@ async def process_yookassa_sbp_payment_amount(
}
)
# Проверяем результат создания платежа
if not payment_result:
await message.answer("❌ Ошибка создания платежа через СБП. Попробуйте позже или обратитесь в поддержку.")
await state.clear()
return
# Получаем URL для подтверждения платежа
confirmation_url = payment_result.get("confirmation_url")
if not confirmation_url:
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")]
])
# Отправляем сообщение с инструкцией по оплате
await message.answer(
f"🏦 <b>Оплата через СБП</b>\n\n"
f"💰 Сумма: {settings.format_price(amount_kopeks)}\n"
@@ -626,10 +609,8 @@ async def process_yookassa_sbp_payment_amount(
parse_mode="HTML"
)
# Очищаем состояние
await state.clear()
# Логируем успешное создание платежа
logger.info(f"Создан платеж YooKassa СБП для пользователя {db_user.telegram_id}: "
f"{amount_kopeks//100}₽, ID: {payment_result['yookassa_payment_id']}")
@@ -748,7 +729,6 @@ async def process_cryptobot_payment_amount(
amount_rubles = amount_kopeks / 100
# Проверяем лимиты для CryptoBot (оставляем как есть, т.к. это отдельный метод)
if amount_rubles < 100:
await message.answer("Минимальная сумма пополнения: 100 ₽")
return
@@ -758,7 +738,6 @@ async def process_cryptobot_payment_amount(
return
try:
# Получаем курс из состояния или запрашиваем заново
data = await state.get_data()
current_rate = data.get('current_rate')
@@ -766,10 +745,8 @@ async def process_cryptobot_payment_amount(
from app.utils.currency_converter import currency_converter
current_rate = await currency_converter.get_usd_to_rub_rate()
# Конвертируем рубли в доллары
amount_usd = amount_rubles / current_rate
# Округляем до 2 знаков после запятой
amount_usd = round(amount_usd, 2)
if amount_usd < 1:
@@ -897,14 +874,9 @@ async def handle_sbp_payment(
callback: types.CallbackQuery,
db: AsyncSession
):
"""
Обработчик для embedded платежей через СБП
"""
try:
# Получаем ID платежа из callback данных
local_payment_id = int(callback.data.split('_')[-1])
# Получаем информацию о платеже из базы данных
from app.database.crud.yookassa import get_yookassa_payment_by_local_id
payment = await get_yookassa_payment_by_local_id(db, local_payment_id)
@@ -912,7 +884,6 @@ async def handle_sbp_payment(
await callback.answer("❌ Платеж не найден", show_alert=True)
return
# Получаем confirmation_token из метаданных платежа
import json
metadata = json.loads(payment.metadata_json) if payment.metadata_json else {}
confirmation_token = metadata.get("confirmation_token")
@@ -921,7 +892,6 @@ async def handle_sbp_payment(
await callback.answer("❌ Токен подтверждения не найден", show_alert=True)
return
# Отправляем пользователю сообщение с инструкцией по оплате
await callback.message.answer(
f"Для оплаты через СБП откройте приложение вашего банка и подтвердите платеж.\\n\\n"
f"Если у вас не открылось банковское приложение автоматически, вы можете:\\n"
@@ -972,7 +942,6 @@ def register_handlers(dp: Dispatcher):
F.data == "topup_yookassa"
)
# Регистрируем обработчик для кнопки оплаты через СБП
dp.callback_query.register(
start_yookassa_sbp_payment,
F.data == "topup_yookassa_sbp"

View File

@@ -476,7 +476,6 @@ def get_payment_methods_keyboard(amount_kopeks: int, language: str = "ru") -> In
)
])
# Добавляем кнопку для оплаты через СБП, если она включена
if settings.YOOKASSA_SBP_ENABLED:
keyboard.append([
InlineKeyboardButton(

View File

@@ -272,40 +272,21 @@ class PaymentService:
receipt_phone: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None
) -> Optional[Dict[str, Any]]:
"""
Создает платеж через СБП (Систему быстрых платежей) с использованием YooKassa
Args:
db: Сессия базы данных
user_id: ID пользователя
amount_kopeks: Сумма в копейках
description: Описание платежа
receipt_email: Email для чека (опционально)
receipt_phone: Телефон для чека (опционально)
metadata: Метаданные платежа (опционально)
Returns:
Словарь с информацией о платеже или None в случае ошибки
"""
# Проверяем, инициализирован ли YooKassa сервис
if not self.yookassa_service:
logger.error("YooKassa сервис не инициализирован")
return None
try:
# Конвертируем копейки в рубли
amount_rubles = amount_kopeks / 100
# Подготавливаем метаданные платежа
payment_metadata = metadata or {}
payment_metadata.update({
"user_id": str(user_id),
"amount_kopeks": str(amount_kopeks),
"type": "balance_topup_sbp" # Тип платежа - через СБП
"type": "balance_topup_sbp"
})
# Создаем платеж через YooKassa сервис с указанием метода оплаты СБП
yookassa_response = await self.yookassa_service.create_sbp_payment(
amount=amount_rubles,
currency="RUB",
@@ -315,12 +296,10 @@ class PaymentService:
receipt_phone=receipt_phone
)
# Проверяем, успешно ли создан платеж
if not yookassa_response or yookassa_response.get("error"):
logger.error(f"Ошибка создания платежа YooKassa СБП: {yookassa_response}")
return None
# Парсим дату создания платежа
yookassa_created_at = None
if yookassa_response.get("created_at"):
try:
@@ -332,16 +311,13 @@ class PaymentService:
logger.warning(f"Не удалось парсить created_at: {e}")
yookassa_created_at = None
# Получаем confirmation_token для embedded платежей
confirmation_token = None
if yookassa_response.get("confirmation"):
confirmation_token = yookassa_response["confirmation"].get("confirmation_token")
# Обновляем метаданные с confirmation_token если он есть
if confirmation_token:
payment_metadata["confirmation_token"] = confirmation_token
# Сохраняем информацию о платеже в локальной базе данных
local_payment = await create_yookassa_payment(
db=db,
user_id=user_id,
@@ -352,15 +328,13 @@ class PaymentService:
status=yookassa_response["status"],
confirmation_url=yookassa_response.get("confirmation_url"),
metadata_json=payment_metadata,
payment_method_type="bank_card", # Для СБП указываем тип bank_card
payment_method_type="bank_card",
yookassa_created_at=yookassa_created_at,
test_mode=yookassa_response.get("test_mode", False)
)
# Логируем успешное создание платежа
logger.info(f"Создан платеж YooKassa СБП {yookassa_response['id']} на {amount_rubles}₽ для пользователя {user_id}")
# Возвращаем информацию о платеже
return {
"local_payment_id": local_payment.id,
"yookassa_payment_id": yookassa_response["id"],
@@ -793,10 +767,8 @@ class PaymentService:
)
if not updated_payment.transaction_id:
# Получаем сумму в USD из платежа
amount_usd = updated_payment.amount_float
# Конвертируем в рубли по текущему курсу с улучшенной обработкой ошибок
try:
amount_rubles = await currency_converter.usd_to_rub(amount_usd)
amount_kopeks = int(amount_rubles * 100)
@@ -808,7 +780,6 @@ class PaymentService:
amount_kopeks = int(amount_usd * 100)
conversion_rate = 1.0
# Проверяем корректность конвертированной суммы
if amount_kopeks <= 0:
logger.error(f"Некорректная сумма после конвертации: {amount_kopeks} копеек для платежа {invoice_id}")
return False

View File

@@ -156,27 +156,11 @@ class YooKassaService:
metadata: Dict[str, Any],
receipt_email: Optional[str] = None,
receipt_phone: Optional[str] = None) -> Optional[Dict[str, Any]]:
"""
Создает платеж через СБП (Систему быстрых платежей) в YooKassa
Args:
amount: Сумма платежа
currency: Валюта платежа
description: Описание платежа
metadata: Метаданные платежа
receipt_email: Email для чека (опционально)
receipt_phone: Телефон для чека (опционально)
Returns:
Словарь с информацией о платеже или None в случае ошибки
"""
# Проверяем, сконфигурирован ли YooKassa
if not self.configured:
logger.error("YooKassa не сконфигурирован. Невозможно создать платеж через СБП.")
return None
# Подготавливаем контактные данные для чека
customer_contact_for_receipt = {}
if receipt_email:
customer_contact_for_receipt["email"] = receipt_email
@@ -193,36 +177,28 @@ class YooKassaService:
}
try:
# Создаем билдер для запроса платежа
builder = PaymentRequestBuilder()
# Устанавливаем сумму платежа
builder.set_amount({
"value": str(round(amount, 2)),
"currency": currency.upper()
})
# Устанавливаем автоматическое подтверждение платежа
builder.set_capture(True)
# Устанавливаем тип подтверждения - для СБП используем redirect с deeplink
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",
@@ -235,36 +211,28 @@ class YooKassaService:
"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 СБП (Idempotence-Key: {idempotence_key}). "
f"Сумма: {amount} {currency}. Метаданные: {metadata}. Чек: {receipt_data_dict}")
# Выполняем запрос к API YooKassa
loop = asyncio.get_running_loop()
response = await loop.run_in_executor(
None, lambda: YooKassaPayment.create(payment_request, idempotence_key))
# Логируем ответ от API
logger.info(
f"Ответ YooKassa Payment.create (СБП): ID={response.id}, Status={response.status}, Paid={response.paid}")
# Возвращаем информацию о платеже
return {
"id": response.id,
"confirmation_url": response.confirmation.confirmation_url if response.confirmation else None,