From 5d610c484dbb229dfe1b863c3e7b998725732777 Mon Sep 17 00:00:00 2001 From: hexdev Date: Wed, 12 Aug 2026 12:54:30 +0700 Subject: [PATCH] feat: advanced catches at payment processing to prevent the payment loss --- routes/payments/pally.py | 74 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 6 deletions(-) diff --git a/routes/payments/pally.py b/routes/payments/pally.py index 4518418..18d03ff 100644 --- a/routes/payments/pally.py +++ b/routes/payments/pally.py @@ -6,7 +6,11 @@ from fastapi.routing import APIRouter from sqlalchemy.ext.asyncio import AsyncSession from core.deps import get_db +from db.models.transactions import BalanceTransaction, BalanceTxType 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="/payments/pally") @@ -82,11 +86,69 @@ async def pally_callback( amount = int(float(BalanceAmount)) if BalanceAmount is not None else int(float(OutSum)) - await process_subscription_purchase( - session, - invoice_id=int(invoice_id_str), - trs_id=TrsId, - amount=amount, - ) + try: + await process_subscription_purchase( + session, + 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 session.rollback() + + invoice_repo = InvoiceRepository(session) + 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" + + user = await UserRepository(session).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 + + balance_before = user.balance + user.balance += amount + invoice.status = InvoiceStatus.PAID + session.add( + BalanceTransaction( + user_id=user.id, + amount=amount, + tx_type=BalanceTxType.DEPOSIT, + balance_before=balance_before, + balance_after=user.balance, + description=( + f"fallback payment credit for invoice {invoice.id} " f"(TrsId: {TrsId})" + ), + ) + ) + await session.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"