# ruff: noqa: N803 import hashlib import hmac import logging import math from aiogram import Bot from fastapi import Depends, Form, HTTPException from fastapi.routing import APIRouter from sqlalchemy.ext.asyncio import AsyncSession from api.core.deps import get_bot, get_db, get_deps from api.core.notifications import successful_payment_notification from config import settings from db.models.bills import DepositStatus from db.models.transactions import BalanceTxType from misc.pally import BillStatus from schemas.di import APIDependenciesDTO from services.admin_notifications import notify_deposit router = APIRouter(prefix="/pally") logger = logging.getLogger(__name__) @router.post("/result") async def pally_callback( # noqa: PLR0911, PLR0912 InvId: str = Form(...), OutSum: str = Form(...), Commission: str = Form(...), TrsId: str = Form(...), Status: str = Form(...), CurrencyIn: str = Form(...), custom: str | None = Form(None), SignatureValue: str = Form(...), # Optional fields for additional information AccountType: str | None = Form(None), AccountNumber: str | None = Form(None), BalanceAmount: str | None = Form(None), BalanceCurrency: str | None = Form(None), PayerPhone: str | None = Form(None), PayerEmail: str | None = Form(None), PayerName: str | None = Form(None), PayerComment: str | None = Form(None), ErrorCode: int | None = Form(None), ErrorMessage: str | None = Form(None), session: AsyncSession = Depends(get_db), bot: Bot = Depends(get_bot), deps: APIDependenciesDTO = Depends(get_deps), ): # FIXED: Use InvId (which contains the order_id from bill creation) instead of custom field # The InvId field contains the db_bill.id that was passed as order_id during bill creation bill_id_str = InvId logger.info( "Pally webhook received - InvId: %s, OutSum: %s, Commission: %s, TrsId: %s, Status: %s, " "CurrencyIn: %s, custom: %s, BalanceAmount: %s, SignatureValue: %s", InvId, OutSum, Commission, TrsId, Status, CurrencyIn, custom, BalanceAmount, SignatureValue, ) # Enhanced logging for failed payments if Status != BillStatus.SUCCESS: logger.warning( "Non-success payment received - Status: %s, ErrorCode: %s, ErrorMessage: %s, TrsId: %s", Status, ErrorCode, ErrorMessage, TrsId, ) # Validate signature raw_string = f"{OutSum}:{InvId}:{settings.pally_token}" expected_signature = hashlib.md5(raw_string.encode("utf-8")).hexdigest().upper() logger.debug("Signature validation for TrsId %s", TrsId) if not hmac.compare_digest(SignatureValue, expected_signature): logger.critical( "SECURITY ALERT: Invalid signature for TrsId %s - Expected: %s, Received: %s", TrsId, expected_signature, SignatureValue, ) raise HTTPException(403, detail="Invalid signature.") # Only process successful payments if Status != BillStatus.SUCCESS: logger.info("Bill %s skipped: status=%s", TrsId, Status) return "OK" logger.info("Processing successfully paid bill %s", TrsId) # Validate bill ID (from InvId field, which contains the order_id from bill creation) if not bill_id_str or not bill_id_str.isdigit(): logger.critical( "Invalid or non-numeric bill ID in InvId field for TrsId %s: '%s'", TrsId, bill_id_str ) return "OK" # Find bill in database bill = await deps.bills_repository.get_bill_by_id(session, int(bill_id_str)) if not bill: logger.critical("Bill %s not found in database for TrsId %s", bill_id_str, TrsId) return "OK" # Check if already processed if bill.status != DepositStatus.PENDING: logger.warning( "Bill %s (TrsId: %s) is already processed with status: %s", bill.id, TrsId, bill.status ) return "OK" try: # Handle fee scenarios: use BalanceAmount if available (net amount after fees), # otherwise use OutSum (gross amount paid by customer) if BalanceAmount is not None: # Customer pays fees - BalanceAmount is the net amount credited to merchant credited_amount = int(float(BalanceAmount)) gross_amount = int(float(OutSum)) logger.info( "Customer-pays-fees payment: bill_id=%s, expected=%s, gross_paid=%s, net_credited=%s", bill.id, bill.amount, gross_amount, credited_amount, ) # Validate that the net credited amount matches our bill amount if bill.amount != credited_amount: logger.error( "Net amount mismatch for bill %s (TrsId: %s) - Expected: %s, Net credited: %s, Gross paid: %s", bill.id, TrsId, bill.amount, credited_amount, gross_amount, ) return "OK" amount = credited_amount # Credit the net amount (without fees) else: # Standard payment - OutSum should match bill amount exactly amount = int(float(OutSum)) logger.info( "Standard payment: bill_id=%s, expected=%s, received=%s", bill.id, bill.amount, amount, ) if bill.amount != amount: logger.error( "Amount mismatch for bill %s (TrsId: %s) - Expected: %s, Received: %s", bill.id, TrsId, bill.amount, amount, ) return "OK" logger.info( "Processing payment: bill_id=%s, user_id=%s, amount=%s", bill.id, bill.creator_id, amount, ) # Credit user balance credited_user = await deps.user_repository.increase_user_balance( session, bill.creator_id, amount=amount, tx_type=BalanceTxType.DEPOSIT, description=f"payment via PALLY (TrsId: {TrsId})", ) await successful_payment_notification(bot, bill.creator_id, amount) if credited_user: await notify_deposit( bot, user_id=bill.creator_id, amount=amount, balance_before=credited_user.balance - amount, balance_after=credited_user.balance, bill_id=bill.id, payment_id=TrsId, ) # Process referral bonus user = await deps.user_repository.get_user_by_id(session, bill.creator_id) referal = user.referal_id if user else None if referal is not None: referal_amount = math.floor(amount * (settings.referal_bonus / 100)) await deps.user_repository.increase_user_balance( session, referal, referal_amount, tx_type=BalanceTxType.REFERRAL_BONUS, description=f"referral reward for user {bill.creator_id} (TrsId: {TrsId})", ) await successful_payment_notification(bot, referal, referal_amount) logger.info( "Referral bonus processed: referrer_id=%s, amount=%s", referal, referal_amount ) logger.info("Payment processing completed successfully for TrsId %s", TrsId) except Exception as e: logger.exception( "CRITICAL ERROR processing payment for TrsId %s, bill_id %s, user_id %s: %s", TrsId, bill.id, bill.creator_id, str(e), ) # Don't return early - still mark as success to prevent retries # The balance operation might have partially succeeded # Update bill status to success await deps.bills_repository.update_bill_status( session, int(bill.id), status=DepositStatus.SUCCESS ) logger.info("Bill %s marked as SUCCESS for TrsId %s", bill.id, TrsId) return "OK"