From 648f4ba4bcfa98f2f62ac9fc2d9dd4bd83311697 Mon Sep 17 00:00:00 2001 From: 3252a8 <3252a8@proton.me> Date: Fri, 22 May 2026 15:57:49 +0300 Subject: [PATCH] feat: show payment provider webhook urls --- .../bot/app/web/admin_api_impl/settings.py | 9 ++ .../bot/app/web/admin_settings_manifest.py | 10 +- backend/bot/payment_providers/__init__.py | 2 + backend/bot/payment_providers/registry.py | 40 ++++++ .../src/admin/sections/SettingsSection.svelte | 114 +++++++++++++++++- frontend/src/styles/admin.css | 79 ++++++++++++ locales/en.json | 5 + locales/ru.json | 5 + tests/test_admin_settings_manifest_i18n.py | 10 ++ tests/test_security.py | 55 +++++++++ 10 files changed, 326 insertions(+), 3 deletions(-) diff --git a/backend/bot/app/web/admin_api_impl/settings.py b/backend/bot/app/web/admin_api_impl/settings.py index 5f13ee4..8b3299d 100644 --- a/backend/bot/app/web/admin_api_impl/settings.py +++ b/backend/bot/app/web/admin_api_impl/settings.py @@ -13,6 +13,7 @@ async def admin_settings_get_route(request: web.Request) -> web.Response: overrides_by_key = {entry["key"]: entry for entry in overrides} fields = manifest_payload() + webhook_base_url = str(settings.WEBHOOK_BASE_URL or "").strip().rstrip("/") sections: Dict[str, Dict[str, Any]] = {} for field in fields: key = field["key"] @@ -34,6 +35,14 @@ async def admin_settings_get_route(request: web.Request) -> web.Response: } if is_secret: response_field["has_value"] = bool(value) + webhook_path = str(response_field.get("webhook_path") or "").strip() + if webhook_path: + if not webhook_path.startswith("/"): + webhook_path = f"/{webhook_path}" + response_field["webhook_path"] = webhook_path + response_field["webhook_base_url_configured"] = bool(webhook_base_url) + if webhook_base_url: + response_field["webhook_url"] = f"{webhook_base_url}{webhook_path}" sections[section_id]["fields"].append(response_field) ordered_sections = sorted(sections.values(), key=lambda s: s["order"]) diff --git a/backend/bot/app/web/admin_settings_manifest.py b/backend/bot/app/web/admin_settings_manifest.py index 447c9fd..0130564 100644 --- a/backend/bot/app/web/admin_settings_manifest.py +++ b/backend/bot/app/web/admin_settings_manifest.py @@ -507,7 +507,11 @@ def manifest_payload() -> List[dict]: same value so existing UIs that only read ``placeholder`` also show the hint inside the empty input. """ - from bot.payment_providers import find_manifest_owner, manifest_field_default + from bot.payment_providers import ( + find_manifest_owner, + manifest_field_default, + provider_webhook_metadata, + ) sections_order = { "general": 1, @@ -531,10 +535,12 @@ def manifest_payload() -> List[dict]: ) default_value: Optional[str] = None + webhook_metadata: Optional[dict] = None owner = find_manifest_owner(field.key) if owner is not None: spec, manifest_field = owner default_value = manifest_field_default(spec, manifest_field) + webhook_metadata = provider_webhook_metadata(spec) placeholder = field.placeholder if not placeholder and default_value: @@ -561,6 +567,8 @@ def manifest_payload() -> List[dict]: } if default_value is not None: item["default"] = default_value + if webhook_metadata: + item.update(webhook_metadata) if field.choices: item["choices"] = [ { diff --git a/backend/bot/payment_providers/__init__.py b/backend/bot/payment_providers/__init__.py index 041440e..5c39619 100644 --- a/backend/bot/payment_providers/__init__.py +++ b/backend/bot/payment_providers/__init__.py @@ -25,6 +25,7 @@ from .registry import ( provider_emoji_map, provider_label_map, provider_telegram_button_text, + provider_webhook_metadata, resolve_provider_presentation, ) @@ -53,5 +54,6 @@ __all__ = [ "provider_telegram_button_text", "provider_emoji_map", "provider_label_map", + "provider_webhook_metadata", "resolve_provider_presentation", ] diff --git a/backend/bot/payment_providers/registry.py b/backend/bot/payment_providers/registry.py index 0ae2b86..194aa62 100644 --- a/backend/bot/payment_providers/registry.py +++ b/backend/bot/payment_providers/registry.py @@ -333,6 +333,46 @@ def find_manifest_owner(key: str) -> Optional[tuple[PaymentProviderSpec, Provide return None +def _webhook_spec_for(spec: PaymentProviderSpec) -> Optional[PaymentProviderSpec]: + if spec.webhook_path and spec.webhook_route: + return spec + if not spec.service_key: + return None + for candidate in PAYMENT_PROVIDER_SPECS: + if ( + candidate.service_key == spec.service_key + and candidate.webhook_path + and candidate.webhook_route + ): + return candidate + return None + + +def provider_webhook_metadata(spec: PaymentProviderSpec) -> Optional[Dict[str, Any]]: + """Return admin-manifest webhook metadata for a provider SPEC. + + Some visible payment buttons share one backing service and webhook route + (for example Platega SBP and Platega Crypto), so presentation-only specs + inherit the route from their service sibling. + """ + webhook_spec = _webhook_spec_for(spec) + if webhook_spec is None or not webhook_spec.webhook_path: + return None + try: + path = str(webhook_spec.webhook_path(None) or "").strip() + except Exception: + return None + if not path: + return None + return { + "provider_id": spec.id, + "provider_label": spec.label, + "webhook_provider_id": webhook_spec.id, + "webhook_path": path, + "webhook_requires_base_url": bool(webhook_spec.webhook_requires_base_url), + } + + def manifest_field_default( spec: PaymentProviderSpec, manifest_field: ProviderManifestField, diff --git a/frontend/src/admin/sections/SettingsSection.svelte b/frontend/src/admin/sections/SettingsSection.svelte index 1c01d90..da0b650 100644 --- a/frontend/src/admin/sections/SettingsSection.svelte +++ b/frontend/src/admin/sections/SettingsSection.svelte @@ -1,5 +1,5 @@ +{#snippet renderWebhookHint(webhook)} + {@const displayValue = webhook.url || webhook.path} +
{displayValue}
+