fix: resolve remaining TOCTOU issues in RioPay, SeverPay and restore paid_at

- RioPay: use create_transaction(commit=False) to keep FOR UPDATE lock,
  replace update_riopay_payment_status with inline assignment + flush,
  add emit_transaction_side_effects after commit
- SeverPay: add db.flush() before _finalize, remove self-assignment,
  add paid_at to both webhook and status-check paths
- Freekassa/KassaAI: add is_paid and paid_at to webhook and status-check
  inline sections (regression from CRUD→inline migration)
- MulenPay: add is_paid and paid_at to webhook inline section
This commit is contained in:
Fringg
2026-03-21 02:10:41 +03:00
parent 82c79c1306
commit afefcc9c07
5 changed files with 35 additions and 9 deletions

View File

@@ -229,6 +229,8 @@ class FreekassaPaymentMixin:
'cur_id': cur_id,
}
payment.status = 'success'
payment.is_paid = True
payment.paid_at = datetime.now(UTC)
payment.callback_payload = callback_payload
payment.freekassa_order_id = intid
if cur_id is not None:
@@ -529,6 +531,8 @@ class FreekassaPaymentMixin:
# Inline field updates — NO intermediate commit that would release FOR UPDATE lock
payment.status = 'success'
payment.is_paid = True
payment.paid_at = datetime.now(UTC)
payment.callback_payload = callback_payload
payment.freekassa_order_id = fk_intid
if target_order.get('curID'):

View File

@@ -222,6 +222,8 @@ class KassaAiPaymentMixin:
'cur_id': cur_id,
}
payment.status = 'success'
payment.is_paid = True
payment.paid_at = datetime.now(UTC)
payment.callback_payload = callback_payload
payment.kassa_ai_order_id = intid
if cur_id is not None:
@@ -508,6 +510,8 @@ class KassaAiPaymentMixin:
# Inline field updates — NO intermediate commit that would release FOR UPDATE lock
payment.status = 'success'
payment.is_paid = True
payment.paid_at = datetime.now(UTC)
payment.callback_payload = callback_payload
payment.kassa_ai_order_id = kai_intid
if target_order.get('curID'):

View File

@@ -225,6 +225,8 @@ class MulenPayPaymentMixin:
if payment_status == 'success':
# Inline field updates — NO intermediate commit that would release FOR UPDATE lock
payment.status = 'success'
payment.is_paid = True
payment.paid_at = datetime.now(UTC)
payment.callback_payload = callback_data
if mulen_payment_id_int is not None and not payment.mulen_payment_id:
payment.mulen_payment_id = mulen_payment_id_int

View File

@@ -323,7 +323,7 @@ class RioPayPaymentMixin:
)
return False
# Создаем транзакцию
# Создаем транзакцию (commit=False to keep FOR UPDATE lock intact)
transaction = await create_transaction(
db,
user_id=payment.user_id,
@@ -334,15 +334,13 @@ class RioPayPaymentMixin:
external_id=str(riopay_order_id) if riopay_order_id else payment.order_id,
is_completed=True,
created_at=getattr(payment, 'created_at', None),
commit=False,
)
# Связываем платеж с транзакцией
await update_riopay_payment_status(
db=db,
payment=payment,
status=payment.status,
transaction_id=transaction.id,
)
# Связываем платеж с транзакцией (inline — no commit to preserve lock)
payment.transaction_id = transaction.id
payment.updated_at = datetime.now(UTC)
await db.flush()
old_balance = user.balance_kopeks
was_first_topup = not user.has_made_first_topup
@@ -365,6 +363,22 @@ class RioPayPaymentMixin:
await db.commit()
# Emit deferred side-effects after atomic commit (events, promo group checks)
try:
from app.database.crud.transaction import emit_transaction_side_effects
await emit_transaction_side_effects(
db,
transaction,
amount_kopeks=payment.amount_kopeks,
user_id=payment.user_id,
type=TransactionType.DEPOSIT,
payment_method=PaymentMethod.RIOPAY,
external_id=str(riopay_order_id) if riopay_order_id else payment.order_id,
)
except Exception as error:
logger.error('Ошибка emit_transaction_side_effects RioPay', error=error)
# Обработка реферального пополнения
try:
from app.services.referral_service import process_referral_topup

View File

@@ -266,9 +266,11 @@ class SeverPayPaymentMixin:
# Inline field assignments to keep FOR UPDATE lock intact
payment.status = internal_status
payment.is_paid = True
payment.paid_at = datetime.now(UTC)
payment.severpay_id = severpay_id or payment.severpay_id
payment.callback_payload = callback_payload
payment.updated_at = datetime.now(UTC)
await db.flush()
return await self._finalize_severpay_payment(db, payment, severpay_id=severpay_id, trigger='webhook')
# Для не-success статусов можно безопасно коммитить
@@ -581,7 +583,7 @@ class SeverPayPaymentMixin:
# Inline field updates — NO intermediate commit that would release FOR UPDATE lock
payment.status = 'success'
payment.is_paid = True
payment.severpay_id = payment.severpay_id
payment.paid_at = datetime.now(UTC)
payment.callback_payload = {
'check_source': 'api',
'severpay_order_data': order_data,