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
+1
View File
@@ -175,6 +175,7 @@ class BuildServicesWiringTests(unittest.TestCase):
"severpay_service",
"wata_service",
"heleket_service",
"paykilla_service",
}
self.assertEqual(set(services), expected_keys)
+1
View File
@@ -76,6 +76,7 @@ def test_shell_installer_prints_remnashop_webhook_checklist():
assert "/webhook/wata" in script
assert "/webhook/cryptopay" in script
assert "/webhook/heleket" in script
assert "/webhook/paykilla" in script
assert "/webhook/freekassa" in script
assert "/webhook/platega" in script
assert "/tg/webhook" in script
+1
View File
@@ -203,6 +203,7 @@ class RemnashopMigrationDocumentationFactsTests(unittest.TestCase):
"/webhook/wata",
"/webhook/cryptopay",
"/webhook/heleket",
"/webhook/paykilla",
"/webhook/freekassa",
"/webhook/platega",
"/tg/webhook",
+4
View File
@@ -64,6 +64,7 @@ _PROVIDER_MODULES = {
"stars": "StarsService",
"wata": "WataService",
"heleket": "HeleketService",
"paykilla": "PaykillaService",
}
@@ -125,6 +126,7 @@ def test_service_keys_and_statuses_come_from_provider_specs():
"stars_service",
"cryptopay_service",
"heleket_service",
"paykilla_service",
}
assert set(pending_statuses()) >= {
"pending",
@@ -136,6 +138,7 @@ def test_service_keys_and_statuses_come_from_provider_specs():
"pending_cryptopay",
"pending_stars",
"pending_heleket",
"pending_paykilla",
}
@@ -152,6 +155,7 @@ def test_provider_labels_and_emojis_include_storage_keys_and_method_aliases():
assert emojis["stars"] == get_provider_spec("stars").default_telegram_emoji
assert emojis["telegram_stars"] == get_provider_spec("stars").default_telegram_emoji
assert emojis["cryptopay"] == get_provider_spec("cryptopay").default_telegram_emoji
assert labels["paykilla"] == "PayKilla"
def test_provider_presentation_resolves_defaults_and_overrides():
+1
View File
@@ -27,6 +27,7 @@ _PROVIDERS_FROM_CALLERS = {
"severpay",
"wata",
"cryptopay",
"paykilla",
"telegram_stars",
}
+16
View File
@@ -185,6 +185,22 @@ def test_remnashop_crypto_provider_currency_is_not_imported_as_setting_override(
}
assert "HELEKET_CURRENCY" not in heleket["overrides"]
paykilla = remnashop_payment_gateway_overrides(
{
"type": "PAYKILLA",
"currency": "USD",
"is_active": True,
"settings": {"public_key": "public", "secret_key": "secret"},
}
)
assert paykilla["overrides"] == {
"PAYKILLA_ENABLED": True,
"PAYKILLA_API_KEY": "public",
"PAYKILLA_SECRET_KEY": "secret",
}
assert "PAYKILLA_CURRENCY" not in paykilla["overrides"]
assert any("source currency was USD" in warning for warning in paykilla["warnings"])
def test_remnashop_unsupported_gateway_is_reported_without_overrides():
result = remnashop_payment_gateway_overrides(
+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):