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 -1
View File
@@ -16,7 +16,7 @@ Remnawave Minishop - Telegram-бот и Web App (Mini App) для продажи
- Web App / Mini App с входом через Telegram или email;
- встроенные инструкции установки в Mini App: личный экран `/install` и публичная ссылка `/s/<token>` для передачи инструкции;
- пробный период, промокоды и реферальная программа;
- оплата через YooKassa, FreeKassa, Platega, SeverPay, Wata, CryptoPay, Heleket и Telegram Stars;
- оплата через YooKassa, FreeKassa, Platega, SeverPay, Wata, CryptoPay, Heleket, PayKilla и Telegram Stars;
- тикеты поддержки в Web App и внешняя ссылка на поддержку;
- раздел "Мои устройства" при включенном `MY_DEVICES_SECTION_ENABLED`.
@@ -369,7 +369,7 @@ SETTINGS_MANIFEST: List[SettingField] = [
"string",
"payments",
"Порядок методов оплаты",
"Через запятую: severpay,freekassa,yookassa,platega,stars,cryptopay,heleket",
"Через запятую: severpay,freekassa,yookassa,platega,stars,cryptopay,heleket,paykilla",
subsection="common",
),
# ─── Trial ─────────────────────────────────────────────────────
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
from typing import Any, Dict, Iterable, List, Mapping, Optional
from . import cryptopay, freekassa, heleket, platega, severpay, stars, wata, yookassa
from . import cryptopay, freekassa, heleket, paykilla, platega, severpay, stars, wata, yookassa
from .base import (
PaymentProviderPresentation,
PaymentProviderSpec,
@@ -21,6 +21,7 @@ PAYMENT_PROVIDER_SPECS: tuple[PaymentProviderSpec, ...] = (
stars.SPEC,
cryptopay.SPEC,
heleket.SPEC,
paykilla.SPEC,
)
@@ -14,6 +14,7 @@ class PaymentContextMixin:
"severpay": "SeverPay",
"wata": "Wata",
"cryptopay": "Crypto Pay",
"paykilla": "PayKilla",
"telegram_stars": "Telegram Stars",
}
+2 -1
View File
@@ -305,7 +305,7 @@ class Settings(BaseSettings):
STARS_ADMIN_ONLY_ENABLED: bool = Field(default=False)
PAYMENT_METHODS_ORDER: Optional[str] = Field(
default=None,
description="Comma-separated list of payment methods to show (e.g., severpay,wata,freekassa,yookassa,platega,stars,cryptopay)", # noqa: E501
description="Comma-separated list of payment methods to show (e.g., severpay,wata,freekassa,yookassa,platega,stars,cryptopay,heleket,paykilla)", # noqa: E501
)
SUBSCRIPTION_PURCHASE_DESCRIPTION_ENABLED: bool = Field(
default=True,
@@ -1026,6 +1026,7 @@ class Settings(BaseSettings):
"stars",
"cryptopay",
"heleket",
"paykilla",
]
# Make sure default_order itself includes every registered spec.
for sid in spec_ids:
+23
View File
@@ -75,6 +75,7 @@ SUPPORTED_REMNASHOP_PROVIDER_TYPES = {
"TELEGRAM_STARS",
"YOOKASSA",
"HELEKET",
"PAYKILLA",
"CRYPTOPAY",
"FREEKASSA",
"PLATEGA",
@@ -93,6 +94,7 @@ PAYMENT_WEBHOOK_PATHS = {
"wata": "/webhook/wata",
"cryptopay": "/webhook/cryptopay",
"heleket": "/webhook/heleket",
"paykilla": "/webhook/paykilla",
"freekassa": "/webhook/freekassa",
"platega": "/webhook/platega",
}
@@ -477,6 +479,26 @@ def remnashop_payment_gateway_overrides(
)
return _provider_mapping_result(gateway_type, ["heleket"], overrides, warnings)
if gateway_type == "PAYKILLA":
_add_override(overrides, "PAYKILLA_ENABLED", active)
_add_override(
overrides,
"PAYKILLA_API_KEY",
settings.get("api_key") or settings.get("public_key") or settings.get("publicKey"),
)
_add_override(
overrides,
"PAYKILLA_SECRET_KEY",
settings.get("secret_key") or settings.get("secretKey"),
)
if currency and currency != "RUB":
warnings.append(
f"PayKilla source currency was {currency}; Minishop keeps payment currency "
"controlled by tariffs/default currency. Configure PAYKILLA_CURRENCY and "
"PAYKILLA_PAYMENT_CURRENCIES manually if this instance needs a different default."
)
return _provider_mapping_result(gateway_type, ["paykilla"], overrides, warnings)
if gateway_type == "FREEKASSA":
_add_override(overrides, "FREEKASSA_ENABLED", active)
_add_override(overrides, "FREEKASSA_MERCHANT_ID", settings.get("shop_id"))
@@ -539,6 +561,7 @@ def remnashop_post_migration_actions(
"wata": "WATA merchant dashboard -> webhook/callback URL",
"cryptopay": "CryptoBot/Crypto Pay app -> webhook URL",
"heleket": "Heleket merchant dashboard -> payment webhook/callback URL",
"paykilla": "PayKilla Dashboard -> Settings -> Webhooks",
"freekassa": "FreeKassa shop settings -> notification/result URL",
"platega": "Platega merchant/project settings -> webhook URL",
}.get(provider_id, "Payment provider dashboard -> webhook/callback URL"),
+35 -1
View File
@@ -197,7 +197,7 @@
| Переменная | Назначение |
| --- | --- |
| `PAYMENT_METHODS_ORDER` | Порядок кнопок оплаты: `severpay,wata,freekassa,platega,yookassa,stars,cryptopay,heleket`. |
| `PAYMENT_METHODS_ORDER` | Порядок кнопок оплаты: `severpay,wata,freekassa,platega,yookassa,stars,cryptopay,heleket,paykilla`. |
| `SUBSCRIPTION_PURCHASE_DESCRIPTION_ENABLED` | Показывать описание подписки перед выбором срока. |
| `SUBSCRIPTION_PURCHASE_DESCRIPTION_RU` / `SUBSCRIPTION_PURCHASE_DESCRIPTION_EN` | Локализованное описание подписки. |
| `PAYMENT_<METHOD>_WEBAPP_LABEL_RU` / `PAYMENT_<METHOD>_WEBAPP_LABEL_EN` | Текст кнопки провайдера в Web App. |
@@ -213,6 +213,7 @@
| `WATA_ENABLED` | Включает Wata. |
| `CRYPTOPAY_ENABLED` | Включает CryptoPay. |
| `HELEKET_ENABLED` | Включает Heleket. |
| `PAYKILLA_ENABLED` | Включает PayKilla. |
Конкретные ключи отображения:
@@ -271,6 +272,12 @@ PAYMENT_HELEKET_WEBAPP_ICON
PAYMENT_HELEKET_TELEGRAM_LABEL_RU
PAYMENT_HELEKET_TELEGRAM_LABEL_EN
PAYMENT_HELEKET_TELEGRAM_EMOJI
PAYMENT_PAYKILLA_WEBAPP_LABEL_RU
PAYMENT_PAYKILLA_WEBAPP_LABEL_EN
PAYMENT_PAYKILLA_WEBAPP_ICON
PAYMENT_PAYKILLA_TELEGRAM_LABEL_RU
PAYMENT_PAYKILLA_TELEGRAM_LABEL_EN
PAYMENT_PAYKILLA_TELEGRAM_EMOJI
```
### YooKassa
@@ -357,6 +364,33 @@ PAYMENT_HELEKET_TELEGRAM_EMOJI
| `HELEKET_VERIFY_WEBHOOK_SIGNATURE` | Проверять подпись webhook. |
| `HELEKET_TRUSTED_IPS` | Список доверенных IP webhook-источников. |
### PayKilla
Для приема оплат нужен API key типа **HMAC** с правом **INVOICE**. Право **WITHDRAWAL** для оплаты подписок не требуется; включайте его только для отдельной интеграции выплат.
Webhook настраивается в PayKilla Dashboard: **Settings -> Webhooks**. Укажите `WEBHOOK_BASE_URL` + `/webhook/paykilla`, например `https://bot.example.com/webhook/paykilla`. Включите события `INVOICE_PAID` и `INVOICE_EXPIRED` как минимум. Рекомендуемый набор галочек: `INVOICE_PAID`, `PAYMENT_COMPLETED`, `PAYMENT_FAILED`, `PAYMENT_OVERPAID`, `PAYMENT_UNDERPAID`, `PAYMENT_PARTIAL`, `INVOICE_EXPIRED`, `COMPLIANCE_FAILED`. Если хотите видеть промежуточные статусы в логах PayKilla, дополнительно включите `INVOICE_CREATED`, `PAYMENT_PENDING`, `TRANSACTION_CONFIRMED` и `TRANSACTION_FINAL`.
| Переменная | Назначение |
| --- | --- |
| `PAYKILLA_BASE_URL` | Базовый URL API, по умолчанию `https://account-api.paykilla.com`. |
| `PAYKILLA_WIDGET_URL` | URL hosted checkout, по умолчанию `https://gopay.paykilla.com`. |
| `PAYKILLA_API_KEY` / `PAYKILLA_V2_API_KEY` | Public HMAC key с правом `INVOICE`. |
| `PAYKILLA_SECRET_KEY` / `PAYKILLA_V2_SECRET_KEY` | Secret HMAC key для подписи API-запросов и проверки webhook. |
| `PAYKILLA_CURRENCY` | Валюта инвойса. Для RUB/USD/EUR/AED/GBP используется `FIAT_BASED`, для остальных - `FIXED_AMOUNT`. |
| `PAYKILLA_PAYMENT_CURRENCIES` | Crypto tickers для оплаты, например `USDTTRC,BTC,ETH`. |
| `PAYKILLA_SUPPORTED_CURRENCIES` | Валюты инвойса, разрешенные в этом магазине. |
| `PAYKILLA_INVOICE_TYPE` | Необязательный override: `FIAT_BASED`, `FIXED_AMOUNT` или `OPEN_AMOUNT`. |
| `PAYKILLA_RETURN_URL` | URL кнопки возврата на checkout-странице. |
| `PAYKILLA_SUCCESS_URL` | URL после успешной оплаты; используется с auto-redirect. |
| `PAYKILLA_CANCEL_URL` | URL после отмены оплаты. |
| `PAYKILLA_LIFETIME_SECONDS` | TTL инвойса, отправляется как `expiredAt`. |
| `PAYKILLA_RECV_WINDOW_MS` | `recvWindow` для подписанных API-запросов. |
| `PAYKILLA_USER_PAYS_SERVICE_FEE` | `true`, если пользователь оплачивает service fee. |
| `PAYKILLA_USER_PAYS_NETWORK_FEE` | `true`, если пользователь оплачивает network fee. |
| `PAYKILLA_VERIFY_WEBHOOK_SIGNATURE` | Проверять `X-API-SIGN` по raw body webhook. |
| `PAYKILLA_WEBHOOK_URL` | Точный публичный webhook URL для проверки подписи, если он отличается от `WEBHOOK_BASE_URL` + `/webhook/paykilla`. |
| `PAYKILLA_TRUSTED_IPS` | Необязательный список доверенных IP webhook-источников. |
## Тарифы и legacy-цены
Рекомендуемый способ настройки тарифов - раздел **Система -> Тарифы** в админке. Он сохраняет JSON в `TARIFFS_CONFIG_PATH`.
+32
View File
@@ -31,6 +31,7 @@
| Wata | `WEBHOOK_BASE_URL` + `/webhook/wata` | Если включена проверка подписи, настройте `WATA_WEBHOOK_VERIFY_SIGNATURE` и `WATA_PUBLIC_KEY`. |
| CryptoPay | `WEBHOOK_BASE_URL` + `/webhook/cryptopay` | Указывается в настройках Crypto Bot / CryptoPay webhook. |
| Heleket | `WEBHOOK_BASE_URL` + `/webhook/heleket` | При необходимости включите `HELEKET_VERIFY_WEBHOOK_SIGNATURE` и `HELEKET_TRUSTED_IPS`. |
| PayKilla | `WEBHOOK_BASE_URL` + `/webhook/paykilla` | Указывается в PayKilla Dashboard -> Settings -> Webhooks; включите события оплаты инвойсов. |
| Telegram Stars | Отдельный платежный webhook не нужен | Stars-события приходят через webhook Telegram-бота: `WEBHOOK_BASE_URL` + `/tg/webhook`. |
После настройки сделайте тестовый платеж и проверьте, что в логах `backend` видно входящий `POST` на нужный путь. Если провайдер сообщает, что адрес недоступен, сначала проверьте DNS/HTTPS и reverse proxy для `WEBHOOK_BASE_URL`, затем убедитесь, что путь начинается ровно с `/webhook/...` без `/api`, `/auth` и frontend-домена.
@@ -153,6 +154,37 @@ Heleket используется для крипто-инвойсов с отд
Справочник переменных: [Heleket](../configuration/env-vars.md#heleket).
## PayKilla
PayKilla используется для крипто-инвойсов V2 через hosted checkout `https://gopay.paykilla.com/{invoice_id}`. API-запросы подписываются HMAC-SHA256, webhook проверяется по заголовку `X-API-SIGN` и raw body.
Какие полномочия нужны API key:
1. В PayKilla Dashboard откройте **Settings -> API keys**.
2. Создайте ключ типа **HMAC**.
3. Для приема оплат включите permission **INVOICE**.
4. Permission **WITHDRAWAL** не нужен для Minishop-платежей; не включайте его без отдельной необходимости выплат.
5. Сохраните `publicKey` в `PAYKILLA_API_KEY`, а `secretKey` в `PAYKILLA_SECRET_KEY`.
Как настроить webhook в PayKilla:
1. Откройте **Settings -> Webhooks**.
2. В URL укажите `WEBHOOK_BASE_URL` + `/webhook/paykilla`, например `https://bot.example.com/webhook/paykilla`.
3. Минимальные галочки: `INVOICE_PAID`, `INVOICE_EXPIRED`.
4. Рекомендуемые галочки для production: `INVOICE_PAID`, `PAYMENT_COMPLETED`, `PAYMENT_FAILED`, `PAYMENT_OVERPAID`, `PAYMENT_UNDERPAID`, `PAYMENT_PARTIAL`, `INVOICE_EXPIRED`, `COMPLIANCE_FAILED`.
5. Опционально включите `INVOICE_CREATED`, `PAYMENT_PENDING`, `TRANSACTION_CONFIRMED` и `TRANSACTION_FINAL`, если нужны промежуточные события в логах.
6. Оставьте `PAYKILLA_VERIFY_WEBHOOK_SIGNATURE=True`. Если публичный URL у PayKilla отличается от `WEBHOOK_BASE_URL` + `/webhook/paykilla`, задайте точное значение в `PAYKILLA_WEBHOOK_URL`.
Что настроить в Minishop:
1. Включите `PAYKILLA_ENABLED`.
2. Укажите `PAYKILLA_API_KEY` и `PAYKILLA_SECRET_KEY`.
3. Проверьте `PAYKILLA_CURRENCY` и `PAYKILLA_PAYMENT_CURRENCIES`, например `USDTTRC,BTC,ETH`.
4. При необходимости задайте `PAYKILLA_SUCCESS_URL`, `PAYKILLA_RETURN_URL` и `PAYKILLA_CANCEL_URL`.
5. Добавьте `paykilla` в `PAYMENT_METHODS_ORDER`, если хотите задать явный порядок кнопок.
Справочник переменных: [PayKilla](../configuration/env-vars.md#paykilla).
## Telegram Stars
Telegram Stars используются напрямую и поддерживаются в legacy-ценах и JSON-каталоге тарифов.
+4 -2
View File
@@ -46,7 +46,7 @@ App, а не веб-кабинетом Remnashop. В Minishop `SUBSCRIPTION_MINI
Платежные провайдеры берутся из таблицы Remnashop `payment_gateways`.
Поддерживаются и автоматически маппятся: Telegram Stars, YooKassa, WATA,
CryptoPay, Heleket, FreeKassa и Platega. Для них importer переносит флаги
CryptoPay, Heleket, PayKilla, FreeKassa и Platega. Для них importer переносит флаги
включения, API-ключи/merchant IDs и прямые технические параметры, без которых
провайдер не сможет работать: YooKassa receipt email/VAT, FreeKassa second
secret/payment method/server IP и Platega payment method.
@@ -55,7 +55,8 @@ Provider currency и supported-currency ограничения не перено
в Minishop валюта платежа управляется тарифами и `DEFAULT_CURRENCY_SYMBOL`.
Если старый gateway Remnashop был настроен на нестандартную валюту, importer
оставит предупреждение в JSON-сводке; проверьте `CRYPTOPAY_ASSET`,
`HELEKET_CURRENCY`, `HELEKET_SUPPORTED_CURRENCIES` или
`HELEKET_CURRENCY`, `HELEKET_SUPPORTED_CURRENCIES`, `PAYKILLA_CURRENCY`,
`PAYKILLA_PAYMENT_CURRENCIES` или
`PLATEGA_SUPPORTED_CURRENCIES` вручную.
Провайдеры YooMoney, Cryptomus, MulenPay, PayMaster, RoboKassa и UrlPay сейчас
@@ -77,6 +78,7 @@ Remnashop может хранить секреты в формате `enc_...`.
- WATA webhook/callback URL: `WEBHOOK_BASE_URL` + `/webhook/wata`;
- CryptoBot/Crypto Pay webhook URL: `WEBHOOK_BASE_URL` + `/webhook/cryptopay`;
- Heleket payment webhook/callback URL: `WEBHOOK_BASE_URL` + `/webhook/heleket`;
- PayKilla webhook URL: `WEBHOOK_BASE_URL` + `/webhook/paykilla`;
- FreeKassa notification/result URL: `WEBHOOK_BASE_URL` + `/webhook/freekassa`;
- Platega webhook URL: `WEBHOOK_BASE_URL` + `/webhook/platega`;
- Telegram webhook `WEBHOOK_BASE_URL` + `/tg/webhook` выставляется ботом
+5
View File
@@ -479,6 +479,11 @@ export const DEMO_DATASET = {
name: "Heleket",
icon: "CreditCard",
},
{
id: "paykilla",
name: "PayKilla",
icon: "Bitcoin",
},
],
referral: {
code: "DEMO0001",
@@ -650,7 +650,7 @@
"section_order": 4,
"subsection": "common",
"label": "Порядок методов оплаты",
"description": "Через запятую: severpay,freekassa,yookassa,platega,stars,cryptopay,heleket",
"description": "Через запятую: severpay,freekassa,yookassa,platega,stars,cryptopay,heleket,paykilla",
"i18n_label_key": "admin_settings_field_payment_methods_order_label",
"i18n_description_key": "admin_settings_field_payment_methods_order_description",
"i18n_subsection_key": "admin_settings_subsection_common",
@@ -3839,6 +3839,692 @@
"overridden": false,
"updated_at": null,
"webhook_base_url_configured": false
},
{
"key": "PAYKILLA_ENABLED",
"type": "bool",
"section": "payments",
"section_order": 4,
"subsection": "PayKilla",
"label": "Enabled",
"description": "",
"i18n_label_key": "admin_settings_field_paykilla_enabled_label",
"i18n_description_key": null,
"i18n_subsection_key": "admin_settings_subsection_paykilla",
"i18n_placeholder_key": null,
"placeholder": "",
"optional": true,
"secret": false,
"mutually_exclusive_key": "PAYKILLA_ADMIN_ONLY_ENABLED",
"provider_id": "paykilla",
"provider_label": "PayKilla",
"webhook_provider_id": "paykilla",
"webhook_path": "/webhook/paykilla",
"webhook_requires_base_url": false,
"value": "",
"overridden": false,
"updated_at": null,
"webhook_base_url_configured": false
},
{
"key": "PAYKILLA_API_KEY",
"type": "string",
"section": "payments",
"section_order": 4,
"subsection": "PayKilla",
"label": "API key",
"description": "PayKilla public HMAC key with INVOICE permission.",
"i18n_label_key": "admin_settings_field_paykilla_api_key_label",
"i18n_description_key": "admin_settings_field_paykilla_api_key_description",
"i18n_subsection_key": "admin_settings_subsection_paykilla",
"i18n_placeholder_key": null,
"placeholder": "",
"optional": true,
"secret": true,
"provider_id": "paykilla",
"provider_label": "PayKilla",
"webhook_provider_id": "paykilla",
"webhook_path": "/webhook/paykilla",
"webhook_requires_base_url": false,
"value": "",
"overridden": false,
"updated_at": null,
"has_value": false,
"webhook_base_url_configured": false
},
{
"key": "PAYKILLA_SECRET_KEY",
"type": "string",
"section": "payments",
"section_order": 4,
"subsection": "PayKilla",
"label": "Secret key",
"description": "PayKilla HMAC secret key. Never expose it client-side.",
"i18n_label_key": "admin_settings_field_paykilla_secret_key_label",
"i18n_description_key": "admin_settings_field_paykilla_secret_key_description",
"i18n_subsection_key": "admin_settings_subsection_paykilla",
"i18n_placeholder_key": null,
"placeholder": "",
"optional": true,
"secret": true,
"provider_id": "paykilla",
"provider_label": "PayKilla",
"webhook_provider_id": "paykilla",
"webhook_path": "/webhook/paykilla",
"webhook_requires_base_url": false,
"value": "",
"overridden": false,
"updated_at": null,
"has_value": false,
"webhook_base_url_configured": false
},
{
"key": "PAYKILLA_BASE_URL",
"type": "url",
"section": "payments",
"section_order": 4,
"subsection": "PayKilla",
"label": "Base URL",
"description": "",
"i18n_label_key": "admin_settings_field_paykilla_base_url_label",
"i18n_description_key": null,
"i18n_subsection_key": "admin_settings_subsection_paykilla",
"i18n_placeholder_key": "admin_settings_field_paykilla_base_url_placeholder",
"placeholder": "https://account-api.paykilla.com",
"optional": true,
"secret": false,
"provider_id": "paykilla",
"provider_label": "PayKilla",
"webhook_provider_id": "paykilla",
"webhook_path": "/webhook/paykilla",
"webhook_requires_base_url": false,
"value": "",
"overridden": false,
"updated_at": null,
"webhook_base_url_configured": false
},
{
"key": "PAYKILLA_WIDGET_URL",
"type": "url",
"section": "payments",
"section_order": 4,
"subsection": "PayKilla",
"label": "Widget URL",
"description": "",
"i18n_label_key": "admin_settings_field_paykilla_widget_url_label",
"i18n_description_key": null,
"i18n_subsection_key": "admin_settings_subsection_paykilla",
"i18n_placeholder_key": "admin_settings_field_paykilla_widget_url_placeholder",
"placeholder": "https://gopay.paykilla.com",
"optional": true,
"secret": false,
"provider_id": "paykilla",
"provider_label": "PayKilla",
"webhook_provider_id": "paykilla",
"webhook_path": "/webhook/paykilla",
"webhook_requires_base_url": false,
"value": "",
"overridden": false,
"updated_at": null,
"webhook_base_url_configured": false
},
{
"key": "PAYKILLA_CURRENCY",
"type": "string",
"section": "payments",
"section_order": 4,
"subsection": "PayKilla",
"label": "Invoice currency",
"description": "Fiat or crypto invoice currency. FIAT_BASED is selected for RUB/USD/EUR/AED/GBP.",
"i18n_label_key": "admin_settings_field_paykilla_currency_label",
"i18n_description_key": "admin_settings_field_paykilla_currency_description",
"i18n_subsection_key": "admin_settings_subsection_paykilla",
"i18n_placeholder_key": "admin_settings_field_paykilla_currency_placeholder",
"placeholder": "RUB",
"optional": true,
"secret": false,
"provider_id": "paykilla",
"provider_label": "PayKilla",
"webhook_provider_id": "paykilla",
"webhook_path": "/webhook/paykilla",
"webhook_requires_base_url": false,
"value": "",
"overridden": false,
"updated_at": null,
"webhook_base_url_configured": false
},
{
"key": "PAYKILLA_SUPPORTED_CURRENCIES",
"type": "string",
"section": "payments",
"section_order": 4,
"subsection": "PayKilla",
"label": "Supported invoice currencies",
"description": "Comma-separated invoice currencies allowed for PayKilla in this shop.",
"i18n_label_key": "admin_settings_field_paykilla_supported_currencies_label",
"i18n_description_key": "admin_settings_field_paykilla_supported_currencies_description",
"i18n_subsection_key": "admin_settings_subsection_paykilla",
"i18n_placeholder_key": "admin_settings_field_paykilla_supported_currencies_placeholder",
"placeholder": "RUB,USD,EUR,AED,GBP,BTC,ETH,TRX,TON,USDTTRC,USDTETH,USDTBSC,USDCETH,USDCBSC,DAIETH,DAIBSC,BNBBSC,ETHBSC,LINKETH,LINKBSC,USDTTON,AAVEETH,MANAETH,SHIBETH",
"optional": true,
"secret": false,
"provider_id": "paykilla",
"provider_label": "PayKilla",
"webhook_provider_id": "paykilla",
"webhook_path": "/webhook/paykilla",
"webhook_requires_base_url": false,
"value": "",
"overridden": false,
"updated_at": null,
"webhook_base_url_configured": false
},
{
"key": "PAYKILLA_PAYMENT_CURRENCIES",
"type": "string",
"section": "payments",
"section_order": 4,
"subsection": "PayKilla",
"label": "Accepted crypto tickers",
"description": "Comma-separated PayKilla tickers sent as paymentCurrencies, e.g. USDTTRC,BTC,ETH.",
"i18n_label_key": "admin_settings_field_paykilla_payment_currencies_label",
"i18n_description_key": "admin_settings_field_paykilla_payment_currencies_description",
"i18n_subsection_key": "admin_settings_subsection_paykilla",
"i18n_placeholder_key": "admin_settings_field_paykilla_payment_currencies_placeholder",
"placeholder": "USDTTRC,BTC,ETH",
"optional": true,
"secret": false,
"provider_id": "paykilla",
"provider_label": "PayKilla",
"webhook_provider_id": "paykilla",
"webhook_path": "/webhook/paykilla",
"webhook_requires_base_url": false,
"value": "",
"overridden": false,
"updated_at": null,
"webhook_base_url_configured": false
},
{
"key": "PAYKILLA_INVOICE_TYPE",
"type": "string",
"section": "payments",
"section_order": 4,
"subsection": "PayKilla",
"label": "Invoice type",
"description": "Optional override: FIAT_BASED, FIXED_AMOUNT, or OPEN_AMOUNT.",
"i18n_label_key": "admin_settings_field_paykilla_invoice_type_label",
"i18n_description_key": "admin_settings_field_paykilla_invoice_type_description",
"i18n_subsection_key": "admin_settings_subsection_paykilla",
"i18n_placeholder_key": null,
"placeholder": "",
"optional": true,
"secret": false,
"provider_id": "paykilla",
"provider_label": "PayKilla",
"webhook_provider_id": "paykilla",
"webhook_path": "/webhook/paykilla",
"webhook_requires_base_url": false,
"choices": [
{
"value": "",
"label": "Auto",
"i18n_label_key": "admin_settings_field_paykilla_invoice_type_choice_default"
},
{
"value": "FIAT_BASED",
"label": "FIAT_BASED",
"i18n_label_key": "admin_settings_field_paykilla_invoice_type_choice_fiat_based"
},
{
"value": "FIXED_AMOUNT",
"label": "FIXED_AMOUNT",
"i18n_label_key": "admin_settings_field_paykilla_invoice_type_choice_fixed_amount"
},
{
"value": "OPEN_AMOUNT",
"label": "OPEN_AMOUNT",
"i18n_label_key": "admin_settings_field_paykilla_invoice_type_choice_open_amount"
}
],
"value": "",
"overridden": false,
"updated_at": null,
"webhook_base_url_configured": false
},
{
"key": "PAYKILLA_RETURN_URL",
"type": "url",
"section": "payments",
"section_order": 4,
"subsection": "PayKilla",
"label": "Return URL",
"description": "",
"i18n_label_key": "admin_settings_field_paykilla_return_url_label",
"i18n_description_key": null,
"i18n_subsection_key": "admin_settings_subsection_paykilla",
"i18n_placeholder_key": null,
"placeholder": "",
"optional": true,
"secret": false,
"provider_id": "paykilla",
"provider_label": "PayKilla",
"webhook_provider_id": "paykilla",
"webhook_path": "/webhook/paykilla",
"webhook_requires_base_url": false,
"value": "",
"overridden": false,
"updated_at": null,
"webhook_base_url_configured": false
},
{
"key": "PAYKILLA_SUCCESS_URL",
"type": "url",
"section": "payments",
"section_order": 4,
"subsection": "PayKilla",
"label": "Success URL",
"description": "",
"i18n_label_key": "admin_settings_field_paykilla_success_url_label",
"i18n_description_key": null,
"i18n_subsection_key": "admin_settings_subsection_paykilla",
"i18n_placeholder_key": null,
"placeholder": "",
"optional": true,
"secret": false,
"provider_id": "paykilla",
"provider_label": "PayKilla",
"webhook_provider_id": "paykilla",
"webhook_path": "/webhook/paykilla",
"webhook_requires_base_url": false,
"value": "",
"overridden": false,
"updated_at": null,
"webhook_base_url_configured": false
},
{
"key": "PAYKILLA_CANCEL_URL",
"type": "url",
"section": "payments",
"section_order": 4,
"subsection": "PayKilla",
"label": "Cancel URL",
"description": "",
"i18n_label_key": "admin_settings_field_paykilla_cancel_url_label",
"i18n_description_key": null,
"i18n_subsection_key": "admin_settings_subsection_paykilla",
"i18n_placeholder_key": null,
"placeholder": "",
"optional": true,
"secret": false,
"provider_id": "paykilla",
"provider_label": "PayKilla",
"webhook_provider_id": "paykilla",
"webhook_path": "/webhook/paykilla",
"webhook_requires_base_url": false,
"value": "",
"overridden": false,
"updated_at": null,
"webhook_base_url_configured": false
},
{
"key": "PAYKILLA_LIFETIME_SECONDS",
"type": "int",
"section": "payments",
"section_order": 4,
"subsection": "PayKilla",
"label": "Invoice lifetime (seconds)",
"description": "Used to send expiredAt to PayKilla.",
"i18n_label_key": "admin_settings_field_paykilla_lifetime_seconds_label",
"i18n_description_key": "admin_settings_field_paykilla_lifetime_seconds_description",
"i18n_subsection_key": "admin_settings_subsection_paykilla",
"i18n_placeholder_key": null,
"placeholder": "",
"optional": true,
"secret": false,
"min": 300,
"max": 2592000,
"provider_id": "paykilla",
"provider_label": "PayKilla",
"webhook_provider_id": "paykilla",
"webhook_path": "/webhook/paykilla",
"webhook_requires_base_url": false,
"value": "",
"overridden": false,
"updated_at": null,
"webhook_base_url_configured": false
},
{
"key": "PAYKILLA_RECV_WINDOW_MS",
"type": "int",
"section": "payments",
"section_order": 4,
"subsection": "PayKilla",
"label": "Request recvWindow (ms)",
"description": "Validity window for signed PayKilla API requests.",
"i18n_label_key": "admin_settings_field_paykilla_recv_window_ms_label",
"i18n_description_key": "admin_settings_field_paykilla_recv_window_ms_description",
"i18n_subsection_key": "admin_settings_subsection_paykilla",
"i18n_placeholder_key": null,
"placeholder": "",
"optional": true,
"secret": false,
"min": 1000,
"max": 60000,
"provider_id": "paykilla",
"provider_label": "PayKilla",
"webhook_provider_id": "paykilla",
"webhook_path": "/webhook/paykilla",
"webhook_requires_base_url": false,
"value": "",
"overridden": false,
"updated_at": null,
"webhook_base_url_configured": false
},
{
"key": "PAYKILLA_USER_PAYS_SERVICE_FEE",
"type": "bool",
"section": "payments",
"section_order": 4,
"subsection": "PayKilla",
"label": "User pays service fee",
"description": "",
"i18n_label_key": "admin_settings_field_paykilla_user_pays_service_fee_label",
"i18n_description_key": null,
"i18n_subsection_key": "admin_settings_subsection_paykilla",
"i18n_placeholder_key": null,
"placeholder": "",
"optional": true,
"secret": false,
"provider_id": "paykilla",
"provider_label": "PayKilla",
"webhook_provider_id": "paykilla",
"webhook_path": "/webhook/paykilla",
"webhook_requires_base_url": false,
"value": "",
"overridden": false,
"updated_at": null,
"webhook_base_url_configured": false
},
{
"key": "PAYKILLA_USER_PAYS_NETWORK_FEE",
"type": "bool",
"section": "payments",
"section_order": 4,
"subsection": "PayKilla",
"label": "User pays network fee",
"description": "",
"i18n_label_key": "admin_settings_field_paykilla_user_pays_network_fee_label",
"i18n_description_key": null,
"i18n_subsection_key": "admin_settings_subsection_paykilla",
"i18n_placeholder_key": null,
"placeholder": "",
"optional": true,
"secret": false,
"provider_id": "paykilla",
"provider_label": "PayKilla",
"webhook_provider_id": "paykilla",
"webhook_path": "/webhook/paykilla",
"webhook_requires_base_url": false,
"value": "",
"overridden": false,
"updated_at": null,
"webhook_base_url_configured": false
},
{
"key": "PAYKILLA_VERIFY_WEBHOOK_SIGNATURE",
"type": "bool",
"section": "payments",
"section_order": 4,
"subsection": "PayKilla",
"label": "Verify webhook signature",
"description": "",
"i18n_label_key": "admin_settings_field_paykilla_verify_webhook_signature_label",
"i18n_description_key": null,
"i18n_subsection_key": "admin_settings_subsection_paykilla",
"i18n_placeholder_key": null,
"placeholder": "",
"optional": true,
"secret": false,
"provider_id": "paykilla",
"provider_label": "PayKilla",
"webhook_provider_id": "paykilla",
"webhook_path": "/webhook/paykilla",
"webhook_requires_base_url": false,
"value": "",
"overridden": false,
"updated_at": null,
"webhook_base_url_configured": false
},
{
"key": "PAYKILLA_WEBHOOK_URL",
"type": "url",
"section": "payments",
"section_order": 4,
"subsection": "PayKilla",
"label": "Exact webhook URL",
"description": "Optional override for signature verification. Leave empty to use WEBHOOK_BASE_URL + /webhook/paykilla.",
"i18n_label_key": "admin_settings_field_paykilla_webhook_url_label",
"i18n_description_key": "admin_settings_field_paykilla_webhook_url_description",
"i18n_subsection_key": "admin_settings_subsection_paykilla",
"i18n_placeholder_key": null,
"placeholder": "",
"optional": true,
"secret": false,
"provider_id": "paykilla",
"provider_label": "PayKilla",
"webhook_provider_id": "paykilla",
"webhook_path": "/webhook/paykilla",
"webhook_requires_base_url": false,
"value": "",
"overridden": false,
"updated_at": null,
"webhook_base_url_configured": false
},
{
"key": "PAYKILLA_TRUSTED_IPS",
"type": "string",
"section": "payments",
"section_order": 4,
"subsection": "PayKilla",
"label": "Trusted IPs",
"description": "Optional comma-separated IP addresses accepted for PayKilla webhooks.",
"i18n_label_key": "admin_settings_field_paykilla_trusted_ips_label",
"i18n_description_key": "admin_settings_field_paykilla_trusted_ips_description",
"i18n_subsection_key": "admin_settings_subsection_paykilla",
"i18n_placeholder_key": null,
"placeholder": "",
"optional": true,
"secret": false,
"provider_id": "paykilla",
"provider_label": "PayKilla",
"webhook_provider_id": "paykilla",
"webhook_path": "/webhook/paykilla",
"webhook_requires_base_url": false,
"value": "",
"overridden": false,
"updated_at": null,
"webhook_base_url_configured": false
},
{
"key": "PAYMENT_PAYKILLA_WEBAPP_LABEL_RU",
"type": "string",
"section": "payments",
"section_order": 4,
"subsection": "PayKilla",
"label": "WebApp button text (RU)",
"description": "Custom Russian text shown in the Web App payment method button.",
"i18n_label_key": "admin_settings_field_payment_paykilla_webapp_label_ru_label",
"i18n_description_key": "admin_settings_field_payment_paykilla_webapp_label_ru_description",
"i18n_subsection_key": "admin_settings_subsection_paykilla",
"i18n_placeholder_key": "admin_settings_field_payment_paykilla_webapp_label_ru_placeholder",
"placeholder": "PayKilla",
"optional": true,
"secret": false,
"default": "PayKilla",
"provider_id": "paykilla",
"provider_label": "PayKilla",
"webhook_provider_id": "paykilla",
"webhook_path": "/webhook/paykilla",
"webhook_requires_base_url": false,
"value": "",
"overridden": false,
"updated_at": null,
"webhook_base_url_configured": false
},
{
"key": "PAYMENT_PAYKILLA_WEBAPP_LABEL_EN",
"type": "string",
"section": "payments",
"section_order": 4,
"subsection": "PayKilla",
"label": "WebApp button text (EN)",
"description": "Custom English text shown in the Web App payment method button.",
"i18n_label_key": "admin_settings_field_payment_paykilla_webapp_label_en_label",
"i18n_description_key": "admin_settings_field_payment_paykilla_webapp_label_en_description",
"i18n_subsection_key": "admin_settings_subsection_paykilla",
"i18n_placeholder_key": "admin_settings_field_payment_paykilla_webapp_label_en_placeholder",
"placeholder": "PayKilla",
"optional": true,
"secret": false,
"default": "PayKilla",
"provider_id": "paykilla",
"provider_label": "PayKilla",
"webhook_provider_id": "paykilla",
"webhook_path": "/webhook/paykilla",
"webhook_requires_base_url": false,
"value": "",
"overridden": false,
"updated_at": null,
"webhook_base_url_configured": false
},
{
"key": "PAYMENT_PAYKILLA_WEBAPP_ICON",
"type": "icon",
"section": "payments",
"section_order": 4,
"subsection": "PayKilla",
"label": "WebApp button icon",
"description": "Lucide icon name rendered inside the Web App payment method button.",
"i18n_label_key": "admin_settings_field_payment_paykilla_webapp_icon_label",
"i18n_description_key": "admin_settings_field_payment_paykilla_webapp_icon_description",
"i18n_subsection_key": "admin_settings_subsection_paykilla",
"i18n_placeholder_key": "admin_settings_field_payment_paykilla_webapp_icon_placeholder",
"placeholder": "Bitcoin",
"optional": true,
"secret": false,
"default": "Bitcoin",
"provider_id": "paykilla",
"provider_label": "PayKilla",
"webhook_provider_id": "paykilla",
"webhook_path": "/webhook/paykilla",
"webhook_requires_base_url": false,
"value": "",
"overridden": false,
"updated_at": null,
"webhook_base_url_configured": false
},
{
"key": "PAYMENT_PAYKILLA_TELEGRAM_LABEL_RU",
"type": "string",
"section": "payments",
"section_order": 4,
"subsection": "PayKilla",
"label": "Telegram button text (RU)",
"description": "Custom Russian text shown in Telegram bot payment buttons.",
"i18n_label_key": "admin_settings_field_payment_paykilla_telegram_label_ru_label",
"i18n_description_key": "admin_settings_field_payment_paykilla_telegram_label_ru_description",
"i18n_subsection_key": "admin_settings_subsection_paykilla",
"i18n_placeholder_key": "admin_settings_field_payment_paykilla_telegram_label_ru_placeholder",
"placeholder": "PayKilla",
"optional": true,
"secret": false,
"default": "PayKilla",
"provider_id": "paykilla",
"provider_label": "PayKilla",
"webhook_provider_id": "paykilla",
"webhook_path": "/webhook/paykilla",
"webhook_requires_base_url": false,
"value": "",
"overridden": false,
"updated_at": null,
"webhook_base_url_configured": false
},
{
"key": "PAYMENT_PAYKILLA_TELEGRAM_LABEL_EN",
"type": "string",
"section": "payments",
"section_order": 4,
"subsection": "PayKilla",
"label": "Telegram button text (EN)",
"description": "Custom English text shown in Telegram bot payment buttons.",
"i18n_label_key": "admin_settings_field_payment_paykilla_telegram_label_en_label",
"i18n_description_key": "admin_settings_field_payment_paykilla_telegram_label_en_description",
"i18n_subsection_key": "admin_settings_subsection_paykilla",
"i18n_placeholder_key": "admin_settings_field_payment_paykilla_telegram_label_en_placeholder",
"placeholder": "PayKilla",
"optional": true,
"secret": false,
"default": "PayKilla",
"provider_id": "paykilla",
"provider_label": "PayKilla",
"webhook_provider_id": "paykilla",
"webhook_path": "/webhook/paykilla",
"webhook_requires_base_url": false,
"value": "",
"overridden": false,
"updated_at": null,
"webhook_base_url_configured": false
},
{
"key": "PAYMENT_PAYKILLA_TELEGRAM_EMOJI",
"type": "string",
"section": "payments",
"section_order": 4,
"subsection": "PayKilla",
"label": "Telegram button emoji",
"description": "Emoji prepended to the Telegram bot payment button when customized.",
"i18n_label_key": "admin_settings_field_payment_paykilla_telegram_emoji_label",
"i18n_description_key": "admin_settings_field_payment_paykilla_telegram_emoji_description",
"i18n_subsection_key": "admin_settings_subsection_paykilla",
"i18n_placeholder_key": null,
"placeholder": "",
"optional": true,
"secret": false,
"default": "",
"provider_id": "paykilla",
"provider_label": "PayKilla",
"webhook_provider_id": "paykilla",
"webhook_path": "/webhook/paykilla",
"webhook_requires_base_url": false,
"value": "",
"overridden": false,
"updated_at": null,
"webhook_base_url_configured": false
},
{
"key": "PAYKILLA_ADMIN_ONLY_ENABLED",
"type": "bool",
"section": "payments",
"section_order": 4,
"subsection": "PayKilla",
"label": "Only for admins",
"description": "Shows this payment method only to users from ADMIN_IDS. Webhooks and provider services remain active for admin test payments.",
"i18n_label_key": "admin_settings_provider_admin_only_label",
"i18n_description_key": "admin_settings_provider_admin_only_description",
"i18n_subsection_key": "admin_settings_subsection_paykilla",
"i18n_placeholder_key": null,
"placeholder": "",
"optional": true,
"secret": false,
"mutually_exclusive_key": "PAYKILLA_ENABLED",
"provider_id": "paykilla",
"provider_label": "PayKilla",
"webhook_provider_id": "paykilla",
"webhook_path": "/webhook/paykilla",
"webhook_requires_base_url": false,
"value": "",
"overridden": false,
"updated_at": null,
"webhook_base_url_configured": false
}
]
},
+1
View File
@@ -874,6 +874,7 @@ remnashop_webhook_checklist() {
printf ' WATA merchant dashboard -> webhook/callback URL: %s/webhook/wata\n' "$base_url"
printf ' CryptoBot/Crypto Pay app -> webhook URL: %s/webhook/cryptopay\n' "$base_url"
printf ' Heleket merchant dashboard -> payment webhook/callback URL: %s/webhook/heleket\n' "$base_url"
printf ' PayKilla Dashboard -> Settings -> Webhooks URL: %s/webhook/paykilla\n' "$base_url"
printf ' FreeKassa shop settings -> notification/result URL: %s/webhook/freekassa\n' "$base_url"
printf ' Platega merchant/project settings -> webhook URL: %s/webhook/platega\n' "$base_url"
printf ' Telegram webhook: %s/tg/webhook (configured automatically on bot startup)\n' "$base_url"
+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):