feat: add PayKilla payment provider
This commit is contained in:
@@ -175,6 +175,7 @@ class BuildServicesWiringTests(unittest.TestCase):
|
||||
"severpay_service",
|
||||
"wata_service",
|
||||
"heleket_service",
|
||||
"paykilla_service",
|
||||
}
|
||||
self.assertEqual(set(services), expected_keys)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -203,6 +203,7 @@ class RemnashopMigrationDocumentationFactsTests(unittest.TestCase):
|
||||
"/webhook/wata",
|
||||
"/webhook/cryptopay",
|
||||
"/webhook/heleket",
|
||||
"/webhook/paykilla",
|
||||
"/webhook/freekassa",
|
||||
"/webhook/platega",
|
||||
"/tg/webhook",
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -27,6 +27,7 @@ _PROVIDERS_FROM_CALLERS = {
|
||||
"severpay",
|
||||
"wata",
|
||||
"cryptopay",
|
||||
"paykilla",
|
||||
"telegram_stars",
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user