150 lines
5.2 KiB
Python
150 lines
5.2 KiB
Python
import hashlib
|
|
import hmac
|
|
import json
|
|
|
|
from aiohttp import web
|
|
|
|
from config import PARITYPAY_SECRET_KEY
|
|
from core.webhook_abuse import (
|
|
get_webhook_client_ip,
|
|
is_webhook_ip_blocked,
|
|
record_webhook_signature_failure,
|
|
)
|
|
from logger import logger
|
|
from services.payments.pipeline import (
|
|
ParsedPayment,
|
|
process_cancelled_payment,
|
|
process_success_payment,
|
|
)
|
|
|
|
|
|
_PROVIDER = "paritypay"
|
|
|
|
|
|
def _build_signature_string(payload: dict) -> str:
|
|
parts: list[str] = []
|
|
for key in sorted(payload.keys()):
|
|
value = payload[key]
|
|
if value is None:
|
|
parts.append("")
|
|
elif isinstance(value, bool):
|
|
parts.append("1" if value else "0")
|
|
else:
|
|
parts.append(str(value))
|
|
return "".join(parts)
|
|
|
|
|
|
def verify_paritypay_signature(payload: dict, signature: str) -> bool:
|
|
try:
|
|
sign_string = _build_signature_string(payload)
|
|
expected = hmac.new(
|
|
PARITYPAY_SECRET_KEY.encode("utf-8"),
|
|
sign_string.encode("utf-8"),
|
|
hashlib.sha256,
|
|
).hexdigest()
|
|
result = hmac.compare_digest(expected, signature)
|
|
if not result:
|
|
logger.error(f"[ParityPay] Signature mismatch. Expected: {expected}, Got: {signature}")
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"[ParityPay] Ошибка проверки подписи: {e}")
|
|
return False
|
|
|
|
|
|
def _parse_tg_id_from_order(order_id: str) -> int | None:
|
|
if not order_id:
|
|
return None
|
|
parts = order_id.split("_")
|
|
if len(parts) < 2:
|
|
return None
|
|
try:
|
|
return int(parts[1])
|
|
except (ValueError, TypeError):
|
|
return None
|
|
|
|
|
|
async def paritypay_webhook(request: web.Request):
|
|
try:
|
|
ip = get_webhook_client_ip(request)
|
|
if await is_webhook_ip_blocked(ip):
|
|
return web.Response(status=429)
|
|
|
|
raw_body = await request.read()
|
|
try:
|
|
data = json.loads(raw_body)
|
|
except Exception as e:
|
|
logger.error(f"[ParityPay] Невалидный JSON в webhook: {e}")
|
|
return web.Response(status=400)
|
|
|
|
signature = request.headers.get("X-SIGNATURE") or request.headers.get("X-Signature") or ""
|
|
if not signature:
|
|
logger.error("[ParityPay] Нет заголовка X-SIGNATURE")
|
|
await record_webhook_signature_failure(ip)
|
|
return web.Response(status=400)
|
|
|
|
if not verify_paritypay_signature(data, signature):
|
|
await record_webhook_signature_failure(ip)
|
|
return web.Response(status=400)
|
|
|
|
logger.info(f"[ParityPay] webhook: {json.dumps(data, ensure_ascii=False)}")
|
|
|
|
order_id = str(data.get("order_id") or "")
|
|
invoice_id = str(data.get("id") or "")
|
|
status = str(data.get("status") or "").upper()
|
|
amount_raw = data.get("amount")
|
|
service = data.get("service")
|
|
|
|
if not order_id:
|
|
logger.error(f"[ParityPay] Пустой order_id в webhook: {data}")
|
|
return web.Response(status=400)
|
|
|
|
try:
|
|
amount = float(amount_raw) if amount_raw is not None else 0.0
|
|
except (TypeError, ValueError):
|
|
amount = 0.0
|
|
|
|
tg_id = _parse_tg_id_from_order(order_id)
|
|
|
|
if status == "PAID":
|
|
if tg_id is None:
|
|
logger.error(f"[ParityPay] Не удалось определить tg_id из order_id={order_id}")
|
|
return web.Response(status=400)
|
|
|
|
metadata_patch = {
|
|
"provider": _PROVIDER,
|
|
"paritypay_invoice_id": invoice_id or None,
|
|
"paritypay_service": service,
|
|
}
|
|
parsed = ParsedPayment(
|
|
payment_id=order_id,
|
|
tg_id=int(tg_id),
|
|
amount=float(amount),
|
|
currency="RUB",
|
|
metadata=metadata_patch,
|
|
)
|
|
result = await process_success_payment(_PROVIDER, parsed, metadata_patch=metadata_patch)
|
|
if not result.ok:
|
|
logger.error(f"[ParityPay] Pipeline вернул ошибку: {result.error}, order_id={order_id}")
|
|
return web.Response(status=500)
|
|
|
|
logger.info(f"[ParityPay] Платёж обработан: tg_id={tg_id}, amount={amount:.2f} ₽, order_id={order_id}")
|
|
return web.Response(status=200, text="OK")
|
|
|
|
if status in ("EXPIRED", "REFUNDED"):
|
|
new_status = "failed" if status == "EXPIRED" else "refunded"
|
|
parsed = ParsedPayment(
|
|
payment_id=order_id,
|
|
tg_id=int(tg_id) if tg_id is not None else None,
|
|
amount=float(amount),
|
|
currency="RUB",
|
|
)
|
|
await process_cancelled_payment(_PROVIDER, parsed, new_status=new_status)
|
|
logger.warning(f"[ParityPay] Транзакция {status}: order_id={order_id}")
|
|
return web.Response(status=200, text="OK")
|
|
|
|
logger.info(f"[ParityPay] Промежуточный статус '{status}' для order_id={order_id}, игнор")
|
|
return web.Response(status=200, text="OK")
|
|
except Exception as e:
|
|
logger.error(f"[ParityPay] Ошибка в webhook: {e}", exc_info=True)
|
|
return web.Response(status=500)
|