import hashlib 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 router = APIRouter(prefix="/pally") logger = logging.getLogger(__name__) @router.post("/result") async def pally_callback( 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 - Expected: %s, Received: %s, Raw: %s", expected_signature, SignatureValue, raw_string) if SignatureValue != expected_signature: logger.critical( "SECURITY ALERT: Invalid signature for TrsId %s - Expected: %s, Received: %s, Raw: %s", TrsId, expected_signature, SignatureValue, raw_string ) 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 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) # 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"