Files
malenia-backend/routes/payments/pally.py

223 lines
7.2 KiB
Python

# ruff: noqa: N803
import hashlib
import hmac
import logging
import math
from fastapi import Depends, Form, HTTPException
from fastapi.routing import APIRouter
from sqlalchemy.ext.asyncio import AsyncSession
from config import cfg
from core.deps import get_db
from db.models.transactions import BalanceTxType
from external.pally import BillStatus
from repositories.invoices import InvoiceRepository
from repositories.users import UserRepository
from schemas.invoices import InvoiceStatus
router = APIRouter(prefix="/payments/pally")
logger = logging.getLogger(__name__)
@router.post("/result")
async def pally_callback( # noqa: PLR0911
*,
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),
):
users_repo = UserRepository(session)
invoice_repo = InvoiceRepository(session)
invoice_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,
)
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}:{cfg.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 invoice_id_str or not invoice_id_str.isdigit():
logger.critical(
"Invalid or non-numeric bill ID in InvId field for TrsId %s: '%s'",
TrsId,
invoice_id_str,
)
return "OK"
# Find bill in database
invoice = await invoice_repo.get_by_id(int(invoice_id_str))
if not invoice:
logger.critical("Bill %s not found in database for TrsId %s", invoice_id_str, TrsId)
return "OK"
# Check if already processed
if invoice.status != InvoiceStatus.ACTIVE:
logger.warning(
"Bill %s (TrsId: %s) is already processed with status: %s",
invoice.id,
TrsId,
invoice.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",
invoice.id,
invoice.amount,
gross_amount,
credited_amount,
)
# Validate that the net credited amount matches our bill amount
if invoice.amount != credited_amount:
logger.error(
"Net amount mismatch for bill %s (TrsId: %s) - Expected: %s, Net credited: %s, Gross paid: %s",
invoice.id,
TrsId,
invoice.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",
invoice.id,
invoice.amount,
amount,
)
if invoice.amount != amount:
logger.error(
"Amount mismatch for bill %s (TrsId: %s) - Expected: %s, Received: %s",
invoice.id,
TrsId,
invoice.amount,
amount,
)
return "OK"
logger.info(
"Processing payment: bill_id=%s, user_id=%s, amount=%s",
invoice.id,
invoice.creator_id,
amount,
)
# Credit user balance
await users_repo.increase_balance(
invoice.creator_id,
amount=amount,
tx_type=BalanceTxType.DEPOSIT,
description=f"payment via PALLY (TrsId: {TrsId})",
)
# Process referral bonus
user = invoice.creator
referal = user.referal
if referal is not None:
referal_amount = math.floor(amount * (cfg.referal_bonus / 100))
await users_repo.increase_balance(
referal,
referal_amount,
tx_type=BalanceTxType.REFERRAL_BONUS,
description=f"referral reward for user {invoice.creator_id} (TrsId: {TrsId})",
)
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,
invoice.id,
invoice.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 invoice_repo.update_status_by_id(int(invoice.id), status=InvoiceStatus.PAID)
logger.info("Bill %s marked as SUCCESS for TrsId %s", invoice.id, TrsId)
return "OK"