223 lines
7.2 KiB
Python
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"
|