Compare commits
4 Commits
2103e6a882
...
5ec9491e22
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ec9491e22 | |||
| 18f994aed9 | |||
| 936d1ad260 | |||
| 2a0f5c454e |
@@ -30,56 +30,134 @@ 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 ""
|
# FIXED: Use InvId (which contains the order_id from bill creation) instead of custom field
|
||||||
|
# The InvId field contains the db_bill.id that was passed as order_id during bill creation
|
||||||
|
bill_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
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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 bill ID (from InvId field, which contains the order_id from bill creation)
|
||||||
logger.critical("Invalid custom field format for TrsId %s", TrsId)
|
if not bill_id_str or not bill_id_str.isdigit():
|
||||||
|
logger.critical(
|
||||||
|
"Invalid or non-numeric bill ID in InvId field for TrsId %s: '%s'",
|
||||||
|
TrsId, bill_id_str
|
||||||
|
)
|
||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
bill = await deps.bills_repository.get_bill_by_id(session, int(oid))
|
# Find bill in database
|
||||||
|
bill = await deps.bills_repository.get_bill_by_id(session, int(bill_id_str))
|
||||||
|
|
||||||
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", bill_id_str, 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))
|
# 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))
|
||||||
|
|
||||||
if bill.amount != amount:
|
logger.info(
|
||||||
logger.warning("requested amount for bill=%s is not equal to db entry", bill.id)
|
"Customer-pays-fees payment: bill_id=%s, expected=%s, gross_paid=%s, net_credited=%s",
|
||||||
return "OK"
|
bill.id, bill.amount, gross_amount, credited_amount
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# 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))
|
||||||
@@ -88,19 +166,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"
|
||||||
|
|||||||
@@ -39,34 +39,63 @@ 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)",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--customer-pays-fees",
|
||||||
|
action="store_true",
|
||||||
|
help="Симулировать сценарий 'клиент платит комиссию'"
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
pally_token = os.environ["PALLY_TOKEN"]
|
pally_token = os.environ["PALLY_TOKEN"]
|
||||||
|
|
||||||
# Эти поля могут быть любыми строками — вебхук их не использует содержательно,
|
# ИСПРАВЛЕНО: InvId должен содержать ID счета из БД (это order_id при создании счета)
|
||||||
# только включает в подпись. Главное чтобы совпадали с тем, что пойдёт в подпись.
|
# Именно InvId используется в webhook handler для поиска счета в БД
|
||||||
payment_id = "test_payment_001"
|
inv_id = str(args.bill_id) # InvId = bill ID из нашей БД (order_id при создании)
|
||||||
bill_id = f"test_bill_{args.bill_id}"
|
trs_id = f"LXZv3R{args.bill_id}" # TrsId - уникальный идентификатор счета в Pally
|
||||||
currency = "RUB"
|
currency = "RUB"
|
||||||
order_id = str(args.bill_id) # order_id == id счёта в нашей БД
|
custom = None # custom field не используется в production (остается None)
|
||||||
|
|
||||||
|
# Симуляция комиссии
|
||||||
|
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(
|
signature = calculate_signature(
|
||||||
payment_id, args.status, str(args.amount), currency, order_id, pally_token
|
f"{gross_amount:.2f}", inv_id, pally_token
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Используем правильные имена полей согласно документации pally.info
|
||||||
payload = {
|
payload = {
|
||||||
"id": payment_id,
|
"InvId": inv_id, # Содержит ID счета из БД
|
||||||
"bill_id": bill_id,
|
"OutSum": f"{gross_amount:.2f}", # Сумма, которую заплатил клиент (с комиссией)
|
||||||
"status": args.status,
|
"Commission": f"{commission:.2f}",
|
||||||
"req_amount": str(args.amount),
|
"TrsId": trs_id,
|
||||||
"currency": currency,
|
"Status": args.status,
|
||||||
"order_id": order_id,
|
"CurrencyIn": currency,
|
||||||
"signature": signature,
|
"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"{args.host}{ENDPOINT}")
|
||||||
print(f"Отправляю вебхук: {payload}\n")
|
print(f"Отправляю вебхук: {payload}\n")
|
||||||
response = httpx.post(f"{args.host}{ENDPOINT}", data=payload)
|
response = httpx.post(f"{args.host}{ENDPOINT}", data=payload)
|
||||||
|
|||||||
Reference in New Issue
Block a user