# ruff: noqa: N803 import logging from fastapi import Depends, Form, HTTPException from fastapi.routing import APIRouter from db.models.transactions import BalanceTxType from db.session import UnitOfWork, get_uow from external.pally import BillStatus from repositories.invoices import InvoiceRepository from repositories.users import UserRepository from schemas.invoices import InvoiceStatus from services.payments import process_subscription_purchase, validate_pally_signature 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(...), 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), uow: UnitOfWork = Depends(get_uow), ): 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, ) if not validate_pally_signature(OutSum, InvId, SignatureValue): logger.critical( "SECURITY ALERT: Invalid signature for TrsId %s", TrsId, ) raise HTTPException(403, detail="Invalid signature.") if Status != BillStatus.SUCCESS: logger.info("Bill %s skipped: status=%s", TrsId, Status) return "OK" invoice_id_str = InvId 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" amount = int(float(BalanceAmount)) if BalanceAmount is not None else int(float(OutSum)) try: await process_subscription_purchase( uow, invoice_id=int(invoice_id_str), trs_id=TrsId, amount=amount, ) except Exception: # The payment is confirmed, so never leave the user without the money # if order/subscription provisioning fails. logger.exception( "Subscription provisioning failed for bill %s (TrsId: %s); " "crediting the user's bonus balance", invoice_id_str, TrsId, ) await uow.rollback() invoice_repo = InvoiceRepository(uow) invoice = await invoice_repo.get_by_id(int(invoice_id_str)) if invoice is None: logger.critical( "Cannot credit fallback balance: bill %s was not found (TrsId: %s)", invoice_id_str, TrsId, ) raise if invoice.status == InvoiceStatus.PAID: return "OK" users_repo = UserRepository(uow) user = await users_repo.get_user_by_id(invoice.creator_id) if user is None: logger.critical( "Cannot credit fallback balance: user %s was not found " "for bill %s (TrsId: %s)", invoice.creator_id, invoice_id_str, TrsId, ) raise await users_repo.increase_balance( user.id, amount, BalanceTxType.DEPOSIT, f"fallback payment credit for invoice {invoice.id} (TrsId: {TrsId})", ) await invoice_repo.update_status_by_id(invoice.id, InvoiceStatus.PAID) await uow.commit() logger.info( "Fallback payment credit processed: user_id=%s, amount=%s, " "invoice_id=%s, TrsId=%s", user.id, amount, invoice.id, TrsId, ) return "OK"