From 5ec9491e2268fb0617417f4885baa068404ca16a Mon Sep 17 00:00:00 2001 From: hexdev Date: Sun, 31 May 2026 15:23:31 +0700 Subject: [PATCH] feat(pally): support customer-pays-fees scenario with BalanceAmount validation --- api/routes/pally.py | 48 +++++++++++++++++++++++++++++-------- tests/send_pally_webhook.py | 33 +++++++++++++++++++++---- 2 files changed, 67 insertions(+), 14 deletions(-) diff --git a/api/routes/pally.py b/api/routes/pally.py index 77bb993..084910c 100644 --- a/api/routes/pally.py +++ b/api/routes/pally.py @@ -51,8 +51,8 @@ async def pally_callback( 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 + "CurrencyIn: %s, custom: %s, BalanceAmount: %s, SignatureValue: %s", + InvId, OutSum, Commission, TrsId, Status, CurrencyIn, custom, BalanceAmount, SignatureValue ) # Enhanced logging for failed payments @@ -105,15 +105,43 @@ async def pally_callback( return "OK" try: - amount = int(float(OutSum)) - - # Validate payment amount - if bill.amount != amount: - logger.error( - "Amount mismatch for bill %s (TrsId: %s) - Expected: %s, Received: %s", - bill.id, TrsId, bill.amount, amount + # 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", + bill.id, bill.amount, gross_amount, credited_amount ) - return "OK" + + # Validate that the net credited amount matches our bill amount + if bill.amount != credited_amount: + logger.error( + "Net amount mismatch for bill %s (TrsId: %s) - Expected: %s, Net credited: %s, Gross paid: %s", + bill.id, TrsId, bill.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", + bill.id, bill.amount, amount + ) + + if bill.amount != amount: + logger.error( + "Amount mismatch for bill %s (TrsId: %s) - Expected: %s, Received: %s", + bill.id, TrsId, bill.amount, amount + ) + return "OK" logger.info("Processing payment: bill_id=%s, user_id=%s, amount=%s", bill.id, bill.creator_id, amount) diff --git a/tests/send_pally_webhook.py b/tests/send_pally_webhook.py index f5eb949..5fd6341 100644 --- a/tests/send_pally_webhook.py +++ b/tests/send_pally_webhook.py @@ -42,6 +42,11 @@ def main(): choices=["SUCCESS", "FAIL", "UNDERPAID", "OVERPAID"], help="Статус платежа (по умолчанию SUCCESS)", ) + parser.add_argument( + "--customer-pays-fees", + action="store_true", + help="Симулировать сценарий 'клиент платит комиссию'" + ) args = parser.parse_args() pally_token = os.environ["PALLY_TOKEN"] @@ -52,24 +57,44 @@ def main(): trs_id = f"LXZv3R{args.bill_id}" # TrsId - уникальный идентификатор счета в Pally currency = "RUB" custom = None # custom field не используется в production (остается None) - commission = "0.00" # Комиссия + + # Симуляция комиссии + if args.customer_pays_fees: + # Симулируем комиссию ~10% (как в вашем продакшене: 50 -> 55.4) + commission_rate = 0.108 # ~10.8% + gross_amount = args.amount * (1 + commission_rate) + commission = gross_amount - args.amount + + print(f"🔄 Симулируется сценарий 'клиент платит комиссию':") + print(f" Сумма заказа: {args.amount} RUB") + print(f" Комиссия: {commission:.2f} RUB") + print(f" Итого к оплате: {gross_amount:.2f} RUB") + print(f" К зачислению: {args.amount} RUB") + else: + gross_amount = args.amount + commission = 0.00 # Сигнатура рассчитывается с использованием InvId (который содержит bill ID) signature = calculate_signature( - str(args.amount), inv_id, pally_token + f"{gross_amount:.2f}", inv_id, pally_token ) # Используем правильные имена полей согласно документации pally.info payload = { "InvId": inv_id, # Содержит ID счета из БД - "OutSum": str(args.amount), - "Commission": commission, + "OutSum": f"{gross_amount:.2f}", # Сумма, которую заплатил клиент (с комиссией) + "Commission": f"{commission:.2f}", "TrsId": trs_id, "Status": args.status, "CurrencyIn": currency, "custom": custom, # None в production "SignatureValue": signature, } + + # Добавляем BalanceAmount только если клиент платит комиссию + if args.customer_pays_fees: + payload["BalanceAmount"] = str(args.amount) # Сумма к зачислению (без комиссии) + payload["BalanceCurrency"] = currency print(f"{args.host}{ENDPOINT}") print(f"Отправляю вебхук: {payload}\n")