fix: sanitize PayKilla invoice text

This commit is contained in:
3252a8
2026-06-04 14:18:35 +03:00
parent c46aaad7e3
commit 5527bf0247
3 changed files with 109 additions and 4 deletions
+82 -4
View File
@@ -81,6 +81,76 @@ _FAILED_EVENTS = {
"PAYMENT_CANCELLED",
"COMPLIANCE_FAILED",
}
_CYRILLIC_TO_LATIN = str.maketrans(
{
"А": "A",
"Б": "B",
"В": "V",
"Г": "G",
"Д": "D",
"Е": "E",
"Ё": "E",
"Ж": "Zh",
"З": "Z",
"И": "I",
"Й": "Y",
"К": "K",
"Л": "L",
"М": "M",
"Н": "N",
"О": "O",
"П": "P",
"Р": "R",
"С": "S",
"Т": "T",
"У": "U",
"Ф": "F",
"Х": "H",
"Ц": "Ts",
"Ч": "Ch",
"Ш": "Sh",
"Щ": "Sch",
"Ъ": "",
"Ы": "Y",
"Ь": "",
"Э": "E",
"Ю": "Yu",
"Я": "Ya",
"а": "a",
"б": "b",
"в": "v",
"г": "g",
"д": "d",
"е": "e",
"ё": "e",
"ж": "zh",
"з": "z",
"и": "i",
"й": "y",
"к": "k",
"л": "l",
"м": "m",
"н": "n",
"о": "o",
"п": "p",
"р": "r",
"с": "s",
"т": "t",
"у": "u",
"ф": "f",
"х": "h",
"ц": "ts",
"ч": "ch",
"ш": "sh",
"щ": "sch",
"ъ": "",
"ы": "y",
"ь": "",
"э": "e",
"ю": "yu",
"я": "ya",
}
)
class PaykillaConfig(ProviderEnvConfig):
@@ -203,14 +273,22 @@ class PaykillaPresentation(ProviderEnvConfig):
TELEGRAM_EMOJI: Optional[str] = None
def _clean_paykilla_text(value: Any, *, fallback: str, max_length: int = 255) -> str:
def _normalize_paykilla_text(value: Any) -> str:
text = str(value or "")
text = text.translate(_CYRILLIC_TO_LATIN)
text = re.sub(r"[-\u2010-\u2015]", " ", text)
text = re.sub(r"[^\w\s.,]", "", text, flags=re.UNICODE)
text = text.encode("ascii", "ignore").decode("ascii")
text = re.sub(r"[^A-Za-z0-9_\s.,]", "", text)
return re.sub(r"\s+", " ", text).strip()
def _clean_paykilla_text(value: Any, *, fallback: str, max_length: int = 255) -> str:
text = _normalize_paykilla_text(value)
fallback_text = _normalize_paykilla_text(fallback) or "Payment"
text = re.sub(r"\s+", " ", text).strip()
if not text:
text = fallback
return text[:max_length].strip() or fallback[:max_length]
text = fallback_text
return text[:max_length].strip() or fallback_text[:max_length].strip() or "Payment"
def _payment_currencies(config: PaykillaConfig) -> List[str]:
+2
View File
@@ -158,6 +158,8 @@ Heleket используется для крипто-инвойсов с отд
PayKilla используется для крипто-инвойсов V2 через hosted checkout `https://gopay.paykilla.com/{invoice_id}`. API-запросы подписываются HMAC-SHA256, webhook проверяется по заголовку `X-API-SIGN` и raw body.
PayKilla строго валидирует текстовые поля invoice. Minishop перед отправкой автоматически заменяет тире пробелами, транслитерирует кириллицу и оставляет только ASCII-буквы, цифры, пробелы, `_`, `.`, `,` в `purpose` и `description`.
Какие полномочия нужны API key:
1. В PayKilla Dashboard откройте **Settings -> API keys**.
+25
View File
@@ -23,6 +23,7 @@ from bot.payment_providers.heleket import HeleketConfig, HeleketService, _comput
from bot.payment_providers.paykilla import (
PaykillaConfig,
PaykillaService,
_clean_paykilla_text,
_sign_query,
_webhook_signature,
)
@@ -219,6 +220,7 @@ class PaykillaServiceTests(unittest.TestCase):
WEBHOOK_BASE_URL="https://shop.example",
trusted_proxies=["127.0.0.1"],
)
service._default_return_url = "test_bot"
return service
def test_sign_query_uses_timestamp_and_recv_window_only(self):
@@ -232,6 +234,29 @@ class PaykillaServiceTests(unittest.TestCase):
self.assertEqual(query, "timestamp=1738800000000&recvWindow=5000")
self.assertEqual(signature, expected)
def test_clean_text_transliterates_russian_description_for_invoice_fields(self):
text = _clean_paykilla_text(
"Оплата подписки на 1 мес. - тариф «Базовый» ✅",
fallback="Payment 556",
)
self.assertEqual(text, "Oplata podpiski na 1 mes. tarif Bazovyy")
self.assertRegex(text, r"^[A-Za-z0-9_\s.,]+$")
def test_invoice_body_uses_ascii_safe_purpose_and_description(self):
service = self._make_service()
body = service._invoice_body(
payment_db_id=556,
amount=100,
currency="RUB",
description="Оплата подписки на 1 мес. - тариф «Базовый» ✅",
)
self.assertEqual(body["purpose"], "Oplata podpiski na 1 mes. tarif Bazovyy")
self.assertEqual(body["description"], body["purpose"])
self.assertRegex(body["purpose"], r"^[A-Za-z0-9_\s.,]+$")
def test_verify_webhook_signature_accepts_raw_body_signature(self):
service = self._make_service()
raw_body = (