import hashlib import hmac import logging import math from datetime import UTC, datetime from config import cfg from db.models.orders import OrderStatus from db.models.transactions import BalanceTransaction, BalanceTxType from external.rw import sync_subscription_by_telegram_id from repositories import AddonsRepository from repositories.invoices import InvoiceRepository from repositories.orders import OrderRepository from repositories.pricing import PricingRepository from repositories.users import UserRepository from schemas.invoices import InvoiceStatus from services.plans import get_pricing_model from services.subscriptions import ( apply_order_now, deduct_order_balance, queue_order_for_later, should_apply_immediately, ) logger = logging.getLogger(__name__) def validate_pally_signature(out_sum: str, inv_id: str, signature_value: str) -> bool: raw_string = f"{out_sum}:{inv_id}:{cfg.pally_token}" expected_signature = hashlib.md5(raw_string.encode("utf-8")).hexdigest().upper() return hmac.compare_digest(signature_value, expected_signature) async def process_subscription_purchase( # noqa: PLR0911, PLR0912 session, *, invoice_id: int, trs_id: str, amount: int, ) -> None: invoice_repo = InvoiceRepository(session) orders_repo = OrderRepository(session) users_repo = UserRepository(session) pricing_repo = PricingRepository(session) invoice = await invoice_repo.get_by_id(invoice_id) if not invoice: logger.critical("Bill %s not found in database for TrsId %s", invoice_id, trs_id) return if invoice.status != InvoiceStatus.ACTIVE: logger.warning( "Bill %s (TrsId: %s) is already processed with status: %s", invoice.id, trs_id, invoice.status, ) return if invoice.amount != amount: logger.error( "Amount mismatch for bill %s (TrsId: %s) - Expected: %s, Received: %s", invoice.id, trs_id, invoice.amount, amount, ) return if invoice.order_id is None: logger.critical("Invoice %s has no linked order for TrsId %s", invoice.id, trs_id) return order = await orders_repo.get_by_id(invoice.order_id) if order is None: logger.critical( "Order not found for bill %s (TrsId: %s, user_id=%s, order_id=%s)", invoice.id, trs_id, invoice.creator_id, invoice.order_id, ) return if order.user_id != invoice.creator_id: logger.critical( "Order %s does not belong to invoice creator %s for TrsId %s", order.id, invoice.creator_id, trs_id, ) return user = await users_repo.get_user_by_id(invoice.creator_id) if user is None: logger.critical("User %s not found for TrsId %s", invoice.creator_id, trs_id) return subscription = user.subscription now = datetime.now(UTC) pricing = await get_pricing_model(AddonsRepository(session), pricing_repo) logger.info( "Processing payment: bill_id=%s, order_id=%s, user_id=%s, amount=%s", invoice.id, order.id, invoice.creator_id, amount, ) order.status = OrderStatus.PAID invoice.status = InvoiceStatus.PAID if order.balance_amount > 0: await deduct_order_balance( session, user=user, order=order, description=f"order {order.id} partial payment from balance", ) if should_apply_immediately( subscription=subscription, order=order, pricing=pricing, now=now, ): applied_subscription = await apply_order_now( session, user=user, order=order, pricing=pricing, now=now ) await sync_subscription_by_telegram_id( expires_at=applied_subscription.expires_at, devices=applied_subscription.devices, telegram_id=user.telegram_id, username=user.username, ) else: if subscription is None: logger.critical( "Cannot queue order %s without subscription for user %s", order.id, user.id ) return await queue_order_for_later(order=order, subscription=subscription, now=now) referal_id = user.referal_id if referal_id is not None: referal_amount = math.floor(amount * (cfg.referal_bonus / 100)) if referal_amount > 0: referal_user = await session.get(type(user), referal_id) if referal_user is not None: session.add( BalanceTransaction( user_id=referal_id, amount=referal_amount, tx_type=BalanceTxType.REFERRAL_BONUS, balance_before=referal_user.balance, balance_after=referal_user.balance + referal_amount, description=f"referral reward for user {invoice.creator_id} (TrsId: {trs_id})", ) ) referal_user.balance += referal_amount logger.info( "Referral bonus processed: referrer_id=%s, amount=%s", referal_id, referal_amount, ) else: logger.warning( "Referrer %s not found for user_id=%s while processing TrsId %s", referal_id, invoice.creator_id, trs_id, ) await session.commit() logger.info("Bill %s marked as PAID for TrsId %s", invoice_id, trs_id)