Files
malenia/api/routes/pally.py

192 lines
7.4 KiB
Python

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"