feat: add PayKilla payment provider

This commit is contained in:
3252a8
2026-06-04 13:32:15 +03:00
parent 1ccda9ebd1
commit c46aaad7e3
20 changed files with 1974 additions and 8 deletions
+80
View File
@@ -1,6 +1,7 @@
import hashlib
import hmac
import json
import time
import unittest
from types import SimpleNamespace
from unittest.mock import AsyncMock, patch
@@ -19,6 +20,12 @@ from bot.app.web.webapp_auth import (
from bot.payment_providers.cryptopay import CryptoPayService
from bot.payment_providers.freekassa import FreeKassaService
from bot.payment_providers.heleket import HeleketConfig, HeleketService, _compute_signature
from bot.payment_providers.paykilla import (
PaykillaConfig,
PaykillaService,
_sign_query,
_webhook_signature,
)
from bot.payment_providers.yookassa import yookassa_webhook_route
from bot.services.email_templates import render_login_code
from bot.utils.request_security import request_client_ip
@@ -199,6 +206,79 @@ class HeleketServiceTests(unittest.TestCase):
self.assertFalse(service._verify_signature({"order_id": "435", "sign": "bad"}))
class PaykillaServiceTests(unittest.TestCase):
def _make_service(self, *, webhook_url: str = "https://shop.example/webhook/paykilla"):
service = PaykillaService.__new__(PaykillaService)
service.config = PaykillaConfig(
ENABLED=True,
API_KEY="public-key",
SECRET_KEY="secret-key",
WEBHOOK_URL=webhook_url,
)
service.settings = SimpleNamespace(
WEBHOOK_BASE_URL="https://shop.example",
trusted_proxies=["127.0.0.1"],
)
return service
def test_sign_query_uses_timestamp_and_recv_window_only(self):
query, signature = _sign_query(1738800000000, 5000, "secret-key")
expected = hmac.new(
b"secret-key",
b"timestamp=1738800000000&recvWindow=5000",
hashlib.sha256,
).hexdigest()
self.assertEqual(query, "timestamp=1738800000000&recvWindow=5000")
self.assertEqual(signature, expected)
def test_verify_webhook_signature_accepts_raw_body_signature(self):
service = self._make_service()
raw_body = (
b'{"id":"evt_1","priority":"HIGH","eventType":"INVOICE_PAID",'
b'"data":{"id":"inv_1","clientOrderId":"42"}}'
)
timestamp = str(int(time.time() * 1000))
signature = _webhook_signature(
timestamp=timestamp,
method="POST",
url="https://shop.example/webhook/paykilla",
raw_body=raw_body,
secret_key="secret-key",
)
request = SimpleNamespace(
headers={
"X-API-KEY": "public-key",
"X-API-TIMESTAMP": timestamp,
"X-API-RECV-WINDOW": "5000",
"X-API-SIGN": signature,
},
method="POST",
scheme="https",
host="shop.example",
path_qs="/webhook/paykilla",
)
self.assertTrue(service._verify_webhook_signature(request, raw_body))
def test_verify_webhook_signature_rejects_invalid_signature(self):
service = self._make_service()
request = SimpleNamespace(
headers={
"X-API-KEY": "public-key",
"X-API-TIMESTAMP": str(int(time.time() * 1000)),
"X-API-RECV-WINDOW": "5000",
"X-API-SIGN": "bad",
},
method="POST",
scheme="https",
host="shop.example",
path_qs="/webhook/paykilla",
)
self.assertFalse(service._verify_webhook_signature(request, b"{}"))
class WebAppSecurityTests(unittest.IsolatedAsyncioTestCase):
class _AsyncSessionFactory:
def __call__(self):