Files
remnawave-bedolaga-telegram…/app/database/crud/cloudpayments.py
Fringg 1f0fef114b refactor: complete structlog migration with contextvars, kwargs, and logging hardening
- Add ContextVarsMiddleware for automatic user_id/chat_id/username binding
  via structlog contextvars (aiogram) and http_method/http_path (FastAPI)
- Use bound_contextvars() context manager instead of clear_contextvars()
  to safely restore previous state instead of wiping all context
- Register ContextVarsMiddleware as outermost middleware (before GlobalError)
  so all error logs include user context
- Replace structlog.get_logger() with structlog.get_logger(__name__) across
  270 calls in 265 files for meaningful logger names
- Switch wrapper_class from BoundLogger to make_filtering_bound_logger()
  for pre-processor level filtering (performance optimization)
- Migrate 1411 %-style positional arg logger calls to structlog kwargs
  style across 161 files via AST script
- Migrate log_rotation_service.py from stdlib logging to structlog
- Add payment module prefixes to TelegramNotifierProcessor.IGNORED_LOGGER_PREFIXES
  and ExcludePaymentFilter.PAYMENT_MODULES to prevent payment data leaking
  to Telegram notifications and general log files
- Fix LoggingMiddleware: add from_user null-safety for channel posts,
  switch time.time() to time.monotonic() for duration measurement
- Remove duplicate logger assignments in purchase.py, config.py,
  inline.py, and admin/payments.py
2026-02-16 09:18:12 +03:00

237 lines
6.4 KiB
Python

"""CRUD operations for CloudPayments payments."""
from __future__ import annotations
from datetime import datetime
from typing import Any
import structlog
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import CloudPaymentsPayment
logger = structlog.get_logger(__name__)
async def create_cloudpayments_payment(
db: AsyncSession,
*,
user_id: int,
invoice_id: str,
amount_kopeks: int,
description: str | None = None,
currency: str = 'RUB',
payment_url: str | None = None,
email: str | None = None,
metadata: dict[str, Any] | None = None,
test_mode: bool = False,
) -> CloudPaymentsPayment:
"""
Create a new CloudPayments payment record.
Args:
db: Database session
user_id: Internal user ID
invoice_id: Unique invoice ID
amount_kopeks: Amount in kopeks
description: Payment description
currency: Currency code (default RUB)
payment_url: Payment widget URL
email: User's email
metadata: Additional metadata
test_mode: Whether this is a test payment
Returns:
Created CloudPaymentsPayment object
"""
payment = CloudPaymentsPayment(
user_id=user_id,
invoice_id=invoice_id,
amount_kopeks=amount_kopeks,
currency=currency,
description=description,
status='pending',
is_paid=False,
payment_url=payment_url,
email=email,
metadata_json=metadata,
test_mode=test_mode,
)
db.add(payment)
await db.flush()
await db.refresh(payment)
logger.debug(
'Created CloudPayments payment: id invoice amount',
payment_id=payment.id,
invoice_id=invoice_id,
amount_kopeks=amount_kopeks,
)
return payment
async def get_cloudpayments_payment_by_invoice_id(
db: AsyncSession,
invoice_id: str,
) -> CloudPaymentsPayment | None:
"""Get CloudPayments payment by invoice ID."""
result = await db.execute(select(CloudPaymentsPayment).where(CloudPaymentsPayment.invoice_id == invoice_id))
return result.scalars().first()
async def get_cloudpayments_payment_by_id(
db: AsyncSession,
payment_id: int,
) -> CloudPaymentsPayment | None:
"""Get CloudPayments payment by internal ID."""
result = await db.execute(select(CloudPaymentsPayment).where(CloudPaymentsPayment.id == payment_id))
return result.scalars().first()
async def get_cloudpayments_payment_by_transaction_id(
db: AsyncSession,
transaction_id_cp: int,
) -> CloudPaymentsPayment | None:
"""Get CloudPayments payment by CloudPayments transaction ID."""
result = await db.execute(
select(CloudPaymentsPayment).where(CloudPaymentsPayment.transaction_id_cp == transaction_id_cp)
)
return result.scalars().first()
async def update_cloudpayments_payment(
db: AsyncSession,
payment_id: int,
**kwargs: Any,
) -> CloudPaymentsPayment | None:
"""
Update CloudPayments payment record.
Args:
db: Database session
payment_id: Internal payment ID
**kwargs: Fields to update
Returns:
Updated payment or None if not found
"""
payment = await get_cloudpayments_payment_by_id(db, payment_id)
if not payment:
return None
for key, value in kwargs.items():
if hasattr(payment, key):
setattr(payment, key, value)
payment.updated_at = datetime.utcnow()
await db.flush()
await db.refresh(payment)
return payment
async def mark_cloudpayments_payment_as_paid(
db: AsyncSession,
payment_id: int,
*,
transaction_id_cp: int | None = None,
token: str | None = None,
card_first_six: str | None = None,
card_last_four: str | None = None,
card_type: str | None = None,
card_exp_date: str | None = None,
email: str | None = None,
callback_payload: dict[str, Any] | None = None,
) -> CloudPaymentsPayment | None:
"""
Mark CloudPayments payment as paid.
Args:
db: Database session
payment_id: Internal payment ID
transaction_id_cp: CloudPayments transaction ID
token: Card token for recurrent payments
card_first_six: First 6 digits of card
card_last_four: Last 4 digits of card
card_type: Card type (Visa, MasterCard, etc.)
card_exp_date: Card expiration date
email: Payer's email
callback_payload: Full webhook payload
Returns:
Updated payment or None if not found
"""
payment = await get_cloudpayments_payment_by_id(db, payment_id)
if not payment:
return None
payment.status = 'completed'
payment.is_paid = True
payment.paid_at = datetime.utcnow()
if transaction_id_cp is not None:
payment.transaction_id_cp = transaction_id_cp
if token:
payment.token = token
if card_first_six:
payment.card_first_six = card_first_six
if card_last_four:
payment.card_last_four = card_last_four
if card_type:
payment.card_type = card_type
if card_exp_date:
payment.card_exp_date = card_exp_date
if email:
payment.email = email
if callback_payload:
payment.callback_payload = callback_payload
payment.updated_at = datetime.utcnow()
await db.flush()
await db.refresh(payment)
logger.info(
'Marked CloudPayments payment as paid: id invoice', payment_id=payment.id, invoice_id=payment.invoice_id
)
return payment
async def link_cloudpayments_payment_to_transaction(
db: AsyncSession,
payment_id: int,
transaction_id: int,
) -> CloudPaymentsPayment | None:
"""Link CloudPayments payment to internal transaction."""
payment = await get_cloudpayments_payment_by_id(db, payment_id)
if not payment:
return None
payment.transaction_id = transaction_id
await db.flush()
await db.refresh(payment)
return payment
async def get_user_cloudpayments_payments(
db: AsyncSession,
user_id: int,
*,
limit: int = 10,
offset: int = 0,
) -> list[CloudPaymentsPayment]:
"""Get CloudPayments payments for a user."""
result = await db.execute(
select(CloudPaymentsPayment)
.where(CloudPaymentsPayment.user_id == user_id)
.order_by(CloudPaymentsPayment.created_at.desc())
.limit(limit)
.offset(offset)
)
return list(result.scalars().all())