fix: добавить create_transaction для 6 потоков оплаты с баланса

- trial_activation_service: create_transaction после списания за триал
- purchase.py: create_transaction для платного триала через бот
- cabinet/subscription.py: create_transaction для продления и триала,
  исправлены transaction=None → реальный объект в 5 уведомлениях
- simple_subscription.py: create_transaction в обоих обработчиках,
  transaction передаётся в admin-уведомление вместо None
This commit is contained in:
Fringg
2026-03-05 10:15:32 +03:00
parent 9f35088788
commit 374907b607
4 changed files with 90 additions and 16 deletions
+34 -9
View File
@@ -21,7 +21,7 @@ from app.database.crud.subscription import (
from app.database.crud.tariff import get_tariff_by_id, get_tariffs_for_user
from app.database.crud.transaction import create_transaction
from app.database.crud.user import subtract_user_balance
from app.database.models import ServerSquad, Subscription, Tariff, TransactionType, User
from app.database.models import PaymentMethod, ServerSquad, Subscription, Tariff, TransactionType, User
from app.services.notification_delivery_service import (
NotificationType,
notification_delivery_service,
@@ -537,11 +537,12 @@ async def renew_subscription(
# Deduct balance (centralized: row-level lock, promo consumption, paid subscription flag)
from app.database.crud.user import subtract_user_balance
renewal_description = f'Продление подписки на {request.period_days} дней' + (f' ({tariff.name})' if tariff else '')
success = await subtract_user_balance(
db,
user,
price_kopeks,
f'Продление подписки на {request.period_days} дней' + (f' ({tariff.name})' if tariff else ''),
renewal_description,
consume_promo_offer=promo_offer_discount_value > 0,
mark_as_paid_subscription=True,
)
@@ -554,6 +555,16 @@ async def renew_subscription(
},
)
# Создаём транзакцию для учёта списания
transaction = await create_transaction(
db,
user_id=user.id,
type=TransactionType.SUBSCRIPTION_PAYMENT,
amount_kopeks=price_kopeks,
description=renewal_description,
payment_method=PaymentMethod.BALANCE,
)
await db.refresh(user, ['subscription'])
# Extend from end_date or now if expired
@@ -628,7 +639,7 @@ async def renew_subscription(
db=db,
user=user,
subscription=user.subscription,
transaction=None,
transaction=transaction,
period_days=request.period_days,
was_trial_conversion=False,
amount_kopeks=price_kopeks,
@@ -1339,11 +1350,12 @@ async def activate_trial(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f'Insufficient balance. Need {price_kopeks / 100:.2f} RUB',
)
trial_description = 'Активация триальной подписки'
success = await subtract_user_balance(
db,
user,
price_kopeks,
'Активация триальной подписки',
trial_description,
mark_as_paid_subscription=True,
)
if not success:
@@ -1351,6 +1363,17 @@ async def activate_trial(
status_code=status.HTTP_402_PAYMENT_REQUIRED,
detail='Failed to charge trial activation fee',
)
# Создаём транзакцию для учёта списания за триал
await create_transaction(
db,
user_id=user.id,
type=TransactionType.SUBSCRIPTION_PAYMENT,
amount_kopeks=price_kopeks,
description=trial_description,
payment_method=PaymentMethod.BALANCE,
)
logger.info('User paid kopeks for trial activation', user_id=user.id, price_kopeks=price_kopeks)
# Get trial parameters from tariff if configured (same logic as bot handler)
@@ -1830,7 +1853,7 @@ async def submit_purchase(
db=db,
user=user,
subscription=subscription,
transaction=None,
transaction=result.get('transaction'),
period_days=selection.period.days,
was_trial_conversion=result.get('was_trial_conversion', False),
amount_kopeks=pricing.final_total,
@@ -2130,12 +2153,13 @@ async def purchase_tariff(
)
# Create transaction
await create_transaction(
transaction = await create_transaction(
db=db,
user_id=user.id,
type=TransactionType.SUBSCRIPTION_PAYMENT,
amount_kopeks=price_kopeks,
description=description,
payment_method=PaymentMethod.BALANCE,
)
if subscription:
@@ -2283,7 +2307,7 @@ async def purchase_tariff(
db=db,
user=user,
subscription=subscription,
transaction=None,
transaction=transaction,
period_days=period_days,
was_trial_conversion=False,
amount_kopeks=price_kopeks,
@@ -4268,12 +4292,13 @@ async def switch_tariff(
)
# Create transaction
await create_transaction(
switch_transaction = await create_transaction(
db=db,
user_id=user.id,
type=TransactionType.SUBSCRIPTION_PAYMENT,
amount_kopeks=upgrade_cost,
description=description,
payment_method=PaymentMethod.BALANCE,
)
# Update subscription
@@ -4357,7 +4382,7 @@ async def switch_tariff(
db=db,
user=user,
subscription=user.subscription,
transaction=None,
transaction=switch_transaction if upgrade_cost > 0 else None,
period_days=remaining_days if remaining_days > 0 else new_period_days,
was_trial_conversion=False,
amount_kopeks=upgrade_cost,
+32 -4
View File
@@ -433,11 +433,12 @@ async def handle_simple_subscription_pay_with_balance(
# Списываем средства с баланса пользователя
from app.database.crud.user import subtract_user_balance
purchase_description = f'Оплата подписки на {subscription_params["period_days"]} дней'
success = await subtract_user_balance(
db,
db_user,
price_kopeks,
f'Оплата подписки на {subscription_params["period_days"]} дней',
purchase_description,
consume_promo_offer=False,
mark_as_paid_subscription=True,
)
@@ -446,6 +447,19 @@ async def handle_simple_subscription_pay_with_balance(
await callback.answer('❌ Ошибка списания средств с баланса', show_alert=True)
return
# Создаём транзакцию для учёта списания
from app.database.crud.transaction import create_transaction
from app.database.models import PaymentMethod, TransactionType
transaction = await create_transaction(
db,
user_id=db_user.id,
type=TransactionType.SUBSCRIPTION_PAYMENT,
amount_kopeks=price_kopeks,
description=purchase_description,
payment_method=PaymentMethod.BALANCE,
)
# Проверяем, есть ли у пользователя уже подписка
from app.database.crud.subscription import extend_subscription, get_subscription_by_user_id
@@ -631,7 +645,7 @@ async def handle_simple_subscription_pay_with_balance(
db,
db_user,
subscription,
None, # transaction
transaction,
subscription_params['period_days'],
False, # was_trial_conversion
amount_kopeks=price_kopeks,
@@ -2138,11 +2152,12 @@ async def confirm_simple_subscription_purchase(
# Списываем средства с баланса пользователя
from app.database.crud.user import subtract_user_balance
purchase_description = f'Оплата подписки на {subscription_params["period_days"]} дней'
success = await subtract_user_balance(
db,
db_user,
price_kopeks,
f'Оплата подписки на {subscription_params["period_days"]} дней',
purchase_description,
consume_promo_offer=False,
mark_as_paid_subscription=True,
)
@@ -2151,6 +2166,19 @@ async def confirm_simple_subscription_purchase(
await callback.answer('❌ Ошибка списания средств с баланса', show_alert=True)
return
# Создаём транзакцию для учёта списания
from app.database.crud.transaction import create_transaction
from app.database.models import PaymentMethod, TransactionType
transaction = await create_transaction(
db,
user_id=db_user.id,
type=TransactionType.SUBSCRIPTION_PAYMENT,
amount_kopeks=price_kopeks,
description=purchase_description,
payment_method=PaymentMethod.BALANCE,
)
# Проверяем, есть ли у пользователя уже подписка
from app.database.crud.subscription import extend_subscription, get_subscription_by_user_id
@@ -2336,7 +2364,7 @@ async def confirm_simple_subscription_purchase(
db,
db_user,
subscription,
None, # transaction
transaction,
subscription_params['period_days'],
False, # was_trial_conversion
amount_kopeks=price_kopeks,
+12 -1
View File
@@ -16,7 +16,7 @@ from app.database.crud.subscription import (
)
from app.database.crud.transaction import create_transaction
from app.database.crud.user import subtract_user_balance
from app.database.models import Subscription, SubscriptionStatus, TransactionType, User
from app.database.models import PaymentMethod, Subscription, SubscriptionStatus, TransactionType, User
from app.keyboards.inline import (
get_back_keyboard,
get_countries_keyboard,
@@ -3301,6 +3301,17 @@ async def handle_trial_pay_with_balance(callback: types.CallbackQuery, db_user:
await callback.answer(texts.t('PAYMENT_FAILED', '❌ Не удалось списать средства'), show_alert=True)
return
# Создаём транзакцию для учёта списания за триал
trial_description = texts.t('TRIAL_PAYMENT_DESCRIPTION', 'Оплата пробной подписки')
await create_transaction(
db,
user_id=db_user.id,
type=TransactionType.SUBSCRIPTION_PAYMENT,
amount_kopeks=trial_price_kopeks,
description=trial_description,
payment_method=PaymentMethod.BALANCE,
)
await db.refresh(db_user)
# Сохраняем ID до начала транзакции (на случай detached session)
+12 -2
View File
@@ -7,8 +7,9 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database.crud.subscription import decrement_subscription_server_counts
from app.database.crud.transaction import create_transaction
from app.database.crud.user import add_user_balance, subtract_user_balance
from app.database.models import Subscription, TransactionType, User
from app.database.models import PaymentMethod, Subscription, TransactionType, User
logger = structlog.get_logger(__name__)
@@ -94,7 +95,16 @@ async def charge_trial_activation_if_required(
if not success:
raise TrialPaymentChargeFailed
# subtract_user_balance обновляет пользователя, но на всякий случай приводим к int
# Создаём транзакцию для учёта списания за триал
await create_transaction(
db,
user_id=user.id,
type=TransactionType.SUBSCRIPTION_PAYMENT,
amount_kopeks=price_kopeks,
description=charge_description,
payment_method=PaymentMethod.BALANCE,
)
return int(price_kopeks)