fix(api): enhance pally webhook with comprehensive error handling and logging

This commit is contained in:
2026-05-31 15:11:54 +07:00
parent 2a0f5c454e
commit 936d1ad260
2 changed files with 82 additions and 29 deletions

View File

@@ -30,57 +30,100 @@ async def pally_callback(
CurrencyIn: str = Form(...), CurrencyIn: str = Form(...),
custom: str | None = Form(None), custom: str | None = Form(None),
SignatureValue: str = Form(...), 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), session: AsyncSession = Depends(get_db),
bot: Bot = Depends(get_bot), bot: Bot = Depends(get_bot),
deps: APIDependenciesDTO = Depends(get_deps), deps: APIDependenciesDTO = Depends(get_deps),
): ):
oid = custom or "" oid = custom or ""
logger.info("invid: %s, outsum: %s, commission: %s, TrsId: %s, status: %s, currencyin: %s, custom: %s, sign: %s", InvId, OutSum, Commission, TrsId, Status, CurrencyIn, custom, SignatureValue) logger.info(
"Pally webhook received - InvId: %s, OutSum: %s, Commission: %s, TrsId: %s, Status: %s, "
"CurrencyIn: %s, custom: %s, SignatureValue: %s",
InvId, OutSum, Commission, TrsId, Status, CurrencyIn, custom, 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}" raw_string = f"{OutSum}:{InvId}:{settings.pally_token}"
expected_signature = hashlib.md5(raw_string.encode("utf-8")).hexdigest().upper() 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: if SignatureValue != expected_signature:
logger.critical("Invalid signature for TrsId %s", TrsId) 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.") raise HTTPException(403, detail="Invalid signature.")
# Only process successful payments
if Status != BillStatus.SUCCESS: if Status != BillStatus.SUCCESS:
logger.info("Bill %s skipped: status=%s", TrsId, Status) logger.info("Bill %s skipped: status=%s", TrsId, Status)
return "OK" return "OK"
logger.info("Received successfully paid bill %s", TrsId) logger.info("Processing successfully paid bill %s", TrsId)
if not oid.isdigit(): # Validate custom field (order ID)
logger.critical("Invalid custom field format for TrsId %s: %s", TrsId, custom) if not oid or not oid.isdigit():
logger.critical("Invalid or missing custom field for TrsId %s: '%s'", TrsId, custom)
return "OK" return "OK"
# Find bill in database
bill = await deps.bills_repository.get_bill_by_id(session, int(oid)) bill = await deps.bills_repository.get_bill_by_id(session, int(oid))
if not bill: if not bill:
logger.critical("Bill %s is not found in db", TrsId) logger.critical("Bill %s not found in database for TrsId %s", oid, TrsId)
return "OK" return "OK"
# Check if already processed
if bill.status != DepositStatus.PENDING: if bill.status != DepositStatus.PENDING:
logger.warning("bill=%s is already paid according to the db", bill.id) logger.warning("Bill %s (TrsId: %s) is already processed with status: %s",
bill.id, TrsId, bill.status)
return "OK" return "OK"
try: try:
amount = int(float(OutSum)) amount = int(float(OutSum))
# Validate payment amount
if bill.amount != amount: if bill.amount != amount:
logger.warning("requested amount for bill=%s is not equal to db entry", bill.id) logger.error(
"Amount mismatch for bill %s (TrsId: %s) - Expected: %s, Received: %s",
bill.id, TrsId, bill.amount, amount
)
return "OK" 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( await deps.user_repository.increase_user_balance(
session, session,
bill.creator_id, bill.creator_id,
amount=amount, amount=amount,
tx_type=BalanceTxType.DEPOSIT, tx_type=BalanceTxType.DEPOSIT,
description="personal balance deposit via PALLY", description=f"payment via PALLY (TrsId: {TrsId})",
) )
await successful_payment_notification(bot, bill.creator_id, amount) 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) user = await deps.user_repository.get_user_by_id(session, bill.creator_id)
referal = user.referal_id if user else None referal = user.referal_id if user else None
if referal is not None: if referal is not None:
referal_amount = math.floor(amount * (settings.referal_bonus / 100)) referal_amount = math.floor(amount * (settings.referal_bonus / 100))
@@ -89,19 +132,26 @@ async def pally_callback(
referal, referal,
referal_amount, referal_amount,
tx_type=BalanceTxType.REFERRAL_BONUS, tx_type=BalanceTxType.REFERRAL_BONUS,
description=f"referal reward for referal_id={bill.creator_id}", description=f"referral reward for user {bill.creator_id} (TrsId: {TrsId})",
) )
await successful_payment_notification(bot, referal, referal_amount) await successful_payment_notification(bot, referal, referal_amount)
logger.info("Referral bonus processed: referrer_id=%s, amount=%s", referal, referal_amount)
except Exception: logger.info("Payment processing completed successfully for TrsId %s", TrsId)
except Exception as e:
logger.exception( logger.exception(
"Exception caught while processing balances for TrsId %s (Creator: %s)", "CRITICAL ERROR processing payment for TrsId %s, bill_id %s, user_id %s: %s",
TrsId, TrsId, bill.id, bill.creator_id, str(e)
bill.creator_id,
) )
# 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( await deps.bills_repository.update_bill_status(
session, int(bill.id), status=DepositStatus.SUCCESS session, int(bill.id), status=DepositStatus.SUCCESS
) )
logger.info("Bill %s marked as SUCCESS for TrsId %s", bill.id, TrsId)
return "OK" return "OK"

View File

@@ -39,32 +39,35 @@ def main():
parser.add_argument( parser.add_argument(
"--status", "--status",
default="SUCCESS", default="SUCCESS",
choices=["SUCCESS", "FAIL", "PROCESS"], choices=["SUCCESS", "FAIL", "UNDERPAID", "OVERPAID"],
help="Статус платежа (по умолчанию SUCCESS)", help="Статус платежа (по умолчанию SUCCESS)",
) )
args = parser.parse_args() args = parser.parse_args()
pally_token = os.environ["PALLY_TOKEN"] pally_token = os.environ["PALLY_TOKEN"]
# Эти поля могут быть любыми строками — вебхук их не использует содержательно, # Корректные поля согласно документации pally.info
# только включает в подпись. Главное чтобы совпадали с тем, что пойдёт в подпись. inv_id = f"payment-{args.bill_id}" # InvId - идентификатор платежа
payment_id = "test_payment_001" trs_id = f"LXZv3R{args.bill_id}" # TrsId - уникальный идентификатор счета
bill_id = f"test_bill_{args.bill_id}"
currency = "RUB" currency = "RUB"
order_id = str(args.bill_id) # order_id == id счёта в нашей БД custom = str(args.bill_id) # custom field содержит ID счета в нашей БД
commission = "0.00" # Комиссия
# ИСПРАВЛЕНО: используем inv_id вместо payment_id для сигнатуры
signature = calculate_signature( signature = calculate_signature(
payment_id, args.status, str(args.amount), currency, order_id, pally_token str(args.amount), inv_id, pally_token
) )
# ИСПРАВЛЕНО: используем правильные имена полей согласно документации
payload = { payload = {
"id": payment_id, "InvId": inv_id,
"bill_id": bill_id, "OutSum": str(args.amount),
"status": args.status, "Commission": commission,
"req_amount": str(args.amount), "TrsId": trs_id,
"currency": currency, "Status": args.status,
"order_id": order_id, "CurrencyIn": currency,
"signature": signature, "custom": custom,
"SignatureValue": signature,
} }
print(f"{args.host}{ENDPOINT}") print(f"{args.host}{ENDPOINT}")