refactor: move cryptopay env-config into module
This commit is contained in:
@@ -279,31 +279,6 @@ SETTINGS_MANIFEST: List[SettingField] = [
|
||||
SettingField("PLATEGA_FAILED_URL", "url", "payments", "Failed URL", subsection="Platega"),
|
||||
*_payment_presentation_fields("PLATEGA_SBP", "Platega SBP", default_icon="CreditCard"),
|
||||
*_payment_presentation_fields("PLATEGA_CRYPTO", "Platega Crypto", default_icon="Bitcoin"),
|
||||
# CryptoPay
|
||||
SettingField("CRYPTOPAY_ENABLED", "bool", "payments", "Включена", subsection="CryptoPay"),
|
||||
SettingField(
|
||||
"CRYPTOPAY_TOKEN", "string", "payments", "Token", subsection="CryptoPay", secret=True
|
||||
),
|
||||
SettingField(
|
||||
"CRYPTOPAY_NETWORK",
|
||||
"string",
|
||||
"payments",
|
||||
"Network",
|
||||
"mainnet или testnet",
|
||||
subsection="CryptoPay",
|
||||
),
|
||||
SettingField(
|
||||
"CRYPTOPAY_CURRENCY_TYPE",
|
||||
"string",
|
||||
"payments",
|
||||
"Currency type",
|
||||
"fiat или crypto",
|
||||
subsection="CryptoPay",
|
||||
),
|
||||
SettingField(
|
||||
"CRYPTOPAY_ASSET", "string", "payments", "Asset", placeholder="RUB", subsection="CryptoPay"
|
||||
),
|
||||
*_payment_presentation_fields("CRYPTOPAY", "CryptoPay", default_icon="Bitcoin"),
|
||||
# ─── Trial ─────────────────────────────────────────────────────
|
||||
SettingField("TRIAL_ENABLED", "bool", "trial", "Триал включён"),
|
||||
SettingField("TRIAL_DURATION_DAYS", "int", "trial", "Длительность триала (дней)", min=0),
|
||||
|
||||
@@ -1,11 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Awaitable, Callable, List, Mapping, Optional, Sequence, Type
|
||||
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
def provider_env_file() -> Optional[str]:
|
||||
"""Resolve the env file every provider config should read.
|
||||
|
||||
Tests set ``PROVIDER_ENV_FILE=""`` via conftest so per-provider
|
||||
BaseSettings models don't pick up real credentials from the project's
|
||||
.env. Production reads from ``.env`` as usual.
|
||||
"""
|
||||
value = os.environ.get("PROVIDER_ENV_FILE")
|
||||
if value is None:
|
||||
return ".env"
|
||||
return value or None
|
||||
|
||||
|
||||
class ProviderEnvConfig(BaseSettings):
|
||||
"""Base class for per-provider env-config models.
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ from aiocryptopay import AioCryptoPay, Networks
|
||||
from aiocryptopay.models.update import Update
|
||||
from aiogram import Bot, F, Router, types
|
||||
from aiohttp import web
|
||||
from pydantic import Field, field_validator
|
||||
from pydantic_settings import SettingsConfigDict
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
@@ -17,7 +19,14 @@ from bot.services.subscription_service import SubscriptionService
|
||||
from config.settings import Settings
|
||||
from db.dal import payment_dal
|
||||
|
||||
from .base import PaymentProviderSpec, ServiceFactoryContext, WebAppPaymentContext
|
||||
from .base import (
|
||||
PaymentProviderSpec,
|
||||
ProviderEnvConfig,
|
||||
ProviderManifestField,
|
||||
ServiceFactoryContext,
|
||||
WebAppPaymentContext,
|
||||
provider_env_file,
|
||||
)
|
||||
from .shared import (
|
||||
PaymentSuccessRequest,
|
||||
describe_payment,
|
||||
@@ -38,13 +47,54 @@ logger = logging.getLogger(__name__)
|
||||
_LOG = "cryptopay"
|
||||
|
||||
|
||||
class CryptoPayConfig(ProviderEnvConfig):
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=provider_env_file(),
|
||||
env_file_encoding="utf-8",
|
||||
env_prefix="CRYPTOPAY_",
|
||||
extra="ignore",
|
||||
)
|
||||
|
||||
ENABLED: bool = Field(default=True)
|
||||
TOKEN: Optional[str] = None
|
||||
NETWORK: str = Field(default="mainnet")
|
||||
CURRENCY_TYPE: str = Field(default="fiat")
|
||||
ASSET: str = Field(default="RUB")
|
||||
|
||||
@field_validator("TOKEN", mode="before")
|
||||
@classmethod
|
||||
def _strip_optional(cls, v):
|
||||
if isinstance(v, str) and not v.strip():
|
||||
return None
|
||||
return v
|
||||
|
||||
@property
|
||||
def webhook_path(self) -> str:
|
||||
return "/webhook/cryptopay"
|
||||
|
||||
|
||||
class CryptoPayPresentation(ProviderEnvConfig):
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=provider_env_file(),
|
||||
env_file_encoding="utf-8",
|
||||
env_prefix="PAYMENT_CRYPTOPAY_",
|
||||
extra="ignore",
|
||||
)
|
||||
|
||||
WEBAPP_LABEL_RU: Optional[str] = None
|
||||
WEBAPP_LABEL_EN: Optional[str] = None
|
||||
WEBAPP_ICON: Optional[str] = None
|
||||
TELEGRAM_LABEL_RU: Optional[str] = None
|
||||
TELEGRAM_LABEL_EN: Optional[str] = None
|
||||
TELEGRAM_EMOJI: Optional[str] = None
|
||||
|
||||
|
||||
class CryptoPayService:
|
||||
def __init__(
|
||||
self,
|
||||
token: Optional[str],
|
||||
network: str,
|
||||
bot: Bot,
|
||||
settings: Settings,
|
||||
config: CryptoPayConfig,
|
||||
i18n: JsonI18n,
|
||||
async_session_factory: sessionmaker,
|
||||
subscription_service: SubscriptionService,
|
||||
@@ -52,14 +102,15 @@ class CryptoPayService:
|
||||
):
|
||||
self.bot = bot
|
||||
self.settings = settings
|
||||
self.config = config
|
||||
self.i18n = i18n
|
||||
self.async_session_factory = async_session_factory
|
||||
self.subscription_service = subscription_service
|
||||
self.referral_service = referral_service
|
||||
self.token = token
|
||||
if token:
|
||||
net = Networks.TEST_NET if str(network).lower() == "testnet" else Networks.MAIN_NET
|
||||
self.client = AioCryptoPay(token=token, network=net)
|
||||
self.token = config.TOKEN
|
||||
if self.token:
|
||||
net = Networks.TEST_NET if str(config.NETWORK).lower() == "testnet" else Networks.MAIN_NET
|
||||
self.client = AioCryptoPay(token=self.token, network=net)
|
||||
self.client.register_pay_handler(self._invoice_paid_handler)
|
||||
self.configured = True
|
||||
else:
|
||||
@@ -97,7 +148,7 @@ class CryptoPayService:
|
||||
{
|
||||
"user_id": user_id,
|
||||
"amount": float(amount),
|
||||
"currency": self.settings.CRYPTOPAY_ASSET,
|
||||
"currency": self.config.ASSET,
|
||||
"status": "pending_cryptopay",
|
||||
"description": description,
|
||||
"subscription_duration_months": (
|
||||
@@ -127,13 +178,9 @@ class CryptoPayService:
|
||||
try:
|
||||
invoice = await self.client.create_invoice(
|
||||
amount=amount,
|
||||
currency_type=self.settings.CRYPTOPAY_CURRENCY_TYPE,
|
||||
fiat=self.settings.CRYPTOPAY_ASSET
|
||||
if self.settings.CRYPTOPAY_CURRENCY_TYPE == "fiat"
|
||||
else None,
|
||||
asset=self.settings.CRYPTOPAY_ASSET
|
||||
if self.settings.CRYPTOPAY_CURRENCY_TYPE == "crypto"
|
||||
else None,
|
||||
currency_type=self.config.CURRENCY_TYPE,
|
||||
fiat=self.config.ASSET if self.config.CURRENCY_TYPE == "fiat" else None,
|
||||
asset=self.config.ASSET if self.config.CURRENCY_TYPE == "crypto" else None,
|
||||
description=description,
|
||||
payload=payload,
|
||||
)
|
||||
@@ -284,8 +331,7 @@ async def pay_crypto_callback_handler(
|
||||
return
|
||||
|
||||
if (
|
||||
not settings.CRYPTOPAY_ENABLED
|
||||
or not cryptopay_service
|
||||
not cryptopay_service
|
||||
or not getattr(cryptopay_service, "configured", False)
|
||||
):
|
||||
await notify_service_unavailable(callback, translator)
|
||||
@@ -324,15 +370,16 @@ async def pay_crypto_callback_handler(
|
||||
|
||||
|
||||
def create_service(ctx: ServiceFactoryContext) -> CryptoPayService:
|
||||
bundle = ctx.config_for("cryptopay_service")
|
||||
config = bundle.config if bundle and isinstance(bundle.config, CryptoPayConfig) else CryptoPayConfig()
|
||||
return CryptoPayService(
|
||||
ctx.settings.CRYPTOPAY_TOKEN,
|
||||
ctx.settings.CRYPTOPAY_NETWORK,
|
||||
ctx.bot,
|
||||
ctx.settings,
|
||||
ctx.i18n,
|
||||
ctx.async_session_factory,
|
||||
ctx.subscription_service,
|
||||
ctx.referral_service,
|
||||
bot=ctx.bot,
|
||||
settings=ctx.settings,
|
||||
config=config,
|
||||
i18n=ctx.i18n,
|
||||
async_session_factory=ctx.async_session_factory,
|
||||
subscription_service=ctx.subscription_service,
|
||||
referral_service=ctx.referral_service,
|
||||
)
|
||||
|
||||
|
||||
@@ -354,6 +401,45 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
|
||||
return payment_link_response(payment_url=url, payment_id=None)
|
||||
|
||||
|
||||
_PRESENTATION_MANIFEST = tuple(
|
||||
ProviderManifestField(
|
||||
key=key, type=type_, label=label, description=description,
|
||||
placeholder=placeholder, subsection="CryptoPay",
|
||||
target="presentation", attr=attr,
|
||||
)
|
||||
for key, type_, label, description, placeholder, attr in (
|
||||
("PAYMENT_CRYPTOPAY_WEBAPP_LABEL_RU", "string", "WebApp button text (RU)",
|
||||
"Custom Russian text shown in the Web App payment method button.", "", "WEBAPP_LABEL_RU"),
|
||||
("PAYMENT_CRYPTOPAY_WEBAPP_LABEL_EN", "string", "WebApp button text (EN)",
|
||||
"Custom English text shown in the Web App payment method button.", "", "WEBAPP_LABEL_EN"),
|
||||
("PAYMENT_CRYPTOPAY_WEBAPP_ICON", "icon", "WebApp button icon",
|
||||
"Lucide icon name rendered inside the Web App payment method button.",
|
||||
"Bitcoin", "WEBAPP_ICON"),
|
||||
("PAYMENT_CRYPTOPAY_TELEGRAM_LABEL_RU", "string", "Telegram button text (RU)",
|
||||
"Custom Russian text shown in Telegram bot payment buttons.", "", "TELEGRAM_LABEL_RU"),
|
||||
("PAYMENT_CRYPTOPAY_TELEGRAM_LABEL_EN", "string", "Telegram button text (EN)",
|
||||
"Custom English text shown in Telegram bot payment buttons.", "", "TELEGRAM_LABEL_EN"),
|
||||
("PAYMENT_CRYPTOPAY_TELEGRAM_EMOJI", "string", "Telegram button emoji",
|
||||
"Emoji prepended to the Telegram bot payment button when customized.",
|
||||
"₿", "TELEGRAM_EMOJI"),
|
||||
)
|
||||
)
|
||||
|
||||
_CONFIG_MANIFEST = (
|
||||
ProviderManifestField("CRYPTOPAY_ENABLED", "bool", "Включена",
|
||||
subsection="CryptoPay", attr="ENABLED"),
|
||||
ProviderManifestField("CRYPTOPAY_TOKEN", "string", "Token",
|
||||
subsection="CryptoPay", secret=True, attr="TOKEN"),
|
||||
ProviderManifestField("CRYPTOPAY_NETWORK", "string", "Network",
|
||||
placeholder="mainnet", subsection="CryptoPay", attr="NETWORK"),
|
||||
ProviderManifestField("CRYPTOPAY_CURRENCY_TYPE", "string", "Currency type",
|
||||
description="fiat or crypto.",
|
||||
placeholder="fiat", subsection="CryptoPay", attr="CURRENCY_TYPE"),
|
||||
ProviderManifestField("CRYPTOPAY_ASSET", "string", "Asset",
|
||||
placeholder="RUB", subsection="CryptoPay", attr="ASSET"),
|
||||
)
|
||||
|
||||
|
||||
SPEC = PaymentProviderSpec(
|
||||
id="cryptopay",
|
||||
provider_key="cryptopay",
|
||||
@@ -363,14 +449,17 @@ SPEC = PaymentProviderSpec(
|
||||
webapp_icon="Bitcoin",
|
||||
telegram_labels={"ru": "CryptoBot", "en": "CryptoBot"},
|
||||
pending_status="pending_cryptopay",
|
||||
enabled=lambda settings: settings.CRYPTOPAY_ENABLED,
|
||||
enabled=lambda config: bool(getattr(config, "ENABLED", False)),
|
||||
service_key="cryptopay_service",
|
||||
callback_prefix="pay_crypto",
|
||||
router=router,
|
||||
create_service=create_service,
|
||||
webhook_path=lambda settings: settings.cryptopay_webhook_path,
|
||||
webhook_path=lambda source: "/webhook/cryptopay",
|
||||
webhook_route=cryptopay_webhook_route,
|
||||
create_webapp_payment=create_webapp_payment,
|
||||
emoji="₿",
|
||||
telegram_emoji="₿",
|
||||
config_class=CryptoPayConfig,
|
||||
presentation_class=CryptoPayPresentation,
|
||||
manifest_fields=_CONFIG_MANIFEST + _PRESENTATION_MANIFEST,
|
||||
)
|
||||
|
||||
@@ -28,6 +28,7 @@ from .base import (
|
||||
ProviderManifestField,
|
||||
ServiceFactoryContext,
|
||||
WebAppPaymentContext,
|
||||
provider_env_file,
|
||||
)
|
||||
from .shared import (
|
||||
HttpClientMixin,
|
||||
@@ -56,7 +57,7 @@ _LOG = "freekassa"
|
||||
|
||||
class FreeKassaConfig(ProviderEnvConfig):
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_file=provider_env_file(),
|
||||
env_file_encoding="utf-8",
|
||||
env_prefix="FREEKASSA_",
|
||||
extra="ignore",
|
||||
@@ -104,7 +105,7 @@ class FreeKassaConfig(ProviderEnvConfig):
|
||||
|
||||
class FreeKassaPresentation(ProviderEnvConfig):
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_file=provider_env_file(),
|
||||
env_file_encoding="utf-8",
|
||||
env_prefix="PAYMENT_FREEKASSA_",
|
||||
extra="ignore",
|
||||
|
||||
@@ -25,6 +25,7 @@ from .base import (
|
||||
ProviderManifestField,
|
||||
ServiceFactoryContext,
|
||||
WebAppPaymentContext,
|
||||
provider_env_file,
|
||||
)
|
||||
from .shared import (
|
||||
HttpClientMixin,
|
||||
@@ -60,7 +61,7 @@ class HeleketConfig(ProviderEnvConfig):
|
||||
"""All Heleket-specific env vars. Lives inside the provider module."""
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_file=provider_env_file(),
|
||||
env_file_encoding="utf-8",
|
||||
env_prefix="HELEKET_",
|
||||
extra="ignore",
|
||||
@@ -118,7 +119,7 @@ class HeleketPresentation(ProviderEnvConfig):
|
||||
"""Admin-tunable button text/icon overrides for Heleket."""
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_file=provider_env_file(),
|
||||
env_file_encoding="utf-8",
|
||||
env_prefix="PAYMENT_HELEKET_",
|
||||
extra="ignore",
|
||||
|
||||
@@ -50,6 +50,11 @@ def build_provider_configs() -> Dict[str, ProviderConfigBundle]:
|
||||
Specs with neither ``config_class`` nor ``presentation_class`` are skipped.
|
||||
The result is cached as the process-wide bundle.
|
||||
"""
|
||||
from .base import provider_env_file
|
||||
|
||||
env_file = provider_env_file()
|
||||
init_kwargs = {"_env_file": env_file} if env_file is not None else {"_env_file": None}
|
||||
|
||||
bundles: Dict[str, ProviderConfigBundle] = {}
|
||||
seen: set[str] = set()
|
||||
for spec in PAYMENT_PROVIDER_SPECS:
|
||||
@@ -59,8 +64,8 @@ def build_provider_configs() -> Dict[str, ProviderConfigBundle]:
|
||||
continue
|
||||
seen.add(spec.service_key)
|
||||
bundles[spec.service_key] = ProviderConfigBundle(
|
||||
config=spec.config_class() if spec.config_class else None,
|
||||
presentation=spec.presentation_class() if spec.presentation_class else None,
|
||||
config=spec.config_class(**init_kwargs) if spec.config_class else None,
|
||||
presentation=spec.presentation_class(**init_kwargs) if spec.presentation_class else None,
|
||||
)
|
||||
_provider_configs.clear()
|
||||
_provider_configs.update(bundles)
|
||||
|
||||
@@ -24,6 +24,7 @@ from .base import (
|
||||
ProviderManifestField,
|
||||
ServiceFactoryContext,
|
||||
WebAppPaymentContext,
|
||||
provider_env_file,
|
||||
)
|
||||
from .shared import (
|
||||
HttpClientMixin,
|
||||
@@ -53,7 +54,7 @@ _LOG = "severpay"
|
||||
|
||||
class SeverPayConfig(ProviderEnvConfig):
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_file=provider_env_file(),
|
||||
env_file_encoding="utf-8",
|
||||
env_prefix="SEVERPAY_",
|
||||
extra="ignore",
|
||||
@@ -89,7 +90,7 @@ class SeverPayConfig(ProviderEnvConfig):
|
||||
|
||||
class SeverPayPresentation(ProviderEnvConfig):
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_file=provider_env_file(),
|
||||
env_file_encoding="utf-8",
|
||||
env_prefix="PAYMENT_SEVERPAY_",
|
||||
extra="ignore",
|
||||
|
||||
@@ -27,6 +27,7 @@ from .base import (
|
||||
ProviderManifestField,
|
||||
ServiceFactoryContext,
|
||||
WebAppPaymentContext,
|
||||
provider_env_file,
|
||||
)
|
||||
from .shared import (
|
||||
HttpClientMixin,
|
||||
@@ -58,7 +59,7 @@ _LOG = "wata"
|
||||
|
||||
class WataConfig(ProviderEnvConfig):
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_file=provider_env_file(),
|
||||
env_file_encoding="utf-8",
|
||||
env_prefix="WATA_",
|
||||
extra="ignore",
|
||||
@@ -103,7 +104,7 @@ class WataConfig(ProviderEnvConfig):
|
||||
|
||||
class WataPresentation(ProviderEnvConfig):
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_file=provider_env_file(),
|
||||
env_file_encoding="utf-8",
|
||||
env_prefix="PAYMENT_WATA_",
|
||||
extra="ignore",
|
||||
|
||||
@@ -49,11 +49,6 @@ class PaymentSettings(BaseModel):
|
||||
platega_crypto_method: int
|
||||
platega_return_url: Optional[str]
|
||||
platega_failed_url: Optional[str]
|
||||
cryptopay_enabled: bool
|
||||
cryptopay_token: Optional[str]
|
||||
cryptopay_network: str
|
||||
cryptopay_currency_type: str
|
||||
cryptopay_asset: str
|
||||
|
||||
|
||||
class EmailSettings(BaseModel):
|
||||
@@ -187,11 +182,6 @@ class Settings(BaseSettings):
|
||||
description="Comma-separated list of reverse proxy IPs or CIDRs trusted to forward X-Forwarded-For.", # noqa: E501
|
||||
)
|
||||
|
||||
CRYPTOPAY_TOKEN: Optional[str] = None
|
||||
CRYPTOPAY_NETWORK: str = Field(default="mainnet")
|
||||
CRYPTOPAY_CURRENCY_TYPE: str = Field(default="fiat")
|
||||
CRYPTOPAY_ASSET: str = Field(default="RUB")
|
||||
CRYPTOPAY_ENABLED: bool = Field(default=True)
|
||||
PLATEGA_ENABLED: bool = Field(default=False)
|
||||
PLATEGA_BASE_URL: str = Field(default="https://app.platega.io")
|
||||
PLATEGA_MERCHANT_ID: Optional[str] = None
|
||||
@@ -249,12 +239,6 @@ class Settings(BaseSettings):
|
||||
PAYMENT_STARS_TELEGRAM_LABEL_RU: Optional[str] = None
|
||||
PAYMENT_STARS_TELEGRAM_LABEL_EN: Optional[str] = None
|
||||
PAYMENT_STARS_TELEGRAM_EMOJI: Optional[str] = None
|
||||
PAYMENT_CRYPTOPAY_WEBAPP_LABEL_RU: Optional[str] = None
|
||||
PAYMENT_CRYPTOPAY_WEBAPP_LABEL_EN: Optional[str] = None
|
||||
PAYMENT_CRYPTOPAY_WEBAPP_ICON: Optional[str] = None
|
||||
PAYMENT_CRYPTOPAY_TELEGRAM_LABEL_RU: Optional[str] = None
|
||||
PAYMENT_CRYPTOPAY_TELEGRAM_LABEL_EN: Optional[str] = None
|
||||
PAYMENT_CRYPTOPAY_TELEGRAM_EMOJI: Optional[str] = None
|
||||
|
||||
MONTH_1_ENABLED: bool = Field(default=True, alias="1_MONTH_ENABLED")
|
||||
MONTH_3_ENABLED: bool = Field(default=True, alias="3_MONTHS_ENABLED")
|
||||
@@ -513,11 +497,6 @@ class Settings(BaseSettings):
|
||||
platega_crypto_method=self.PLATEGA_CRYPTO_METHOD,
|
||||
platega_return_url=self.PLATEGA_RETURN_URL,
|
||||
platega_failed_url=self.PLATEGA_FAILED_URL,
|
||||
cryptopay_enabled=self.CRYPTOPAY_ENABLED,
|
||||
cryptopay_token=self.CRYPTOPAY_TOKEN,
|
||||
cryptopay_network=self.CRYPTOPAY_NETWORK,
|
||||
cryptopay_currency_type=self.CRYPTOPAY_CURRENCY_TYPE,
|
||||
cryptopay_asset=self.CRYPTOPAY_ASSET,
|
||||
)
|
||||
|
||||
@computed_field
|
||||
@@ -656,18 +635,6 @@ class Settings(BaseSettings):
|
||||
return f"{base.rstrip('/')}{self.panel_webhook_path}"
|
||||
return None
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def cryptopay_webhook_path(self) -> str:
|
||||
return "/webhook/cryptopay"
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def cryptopay_full_webhook_url(self) -> Optional[str]:
|
||||
base = self.WEBHOOK_BASE_URL
|
||||
if base:
|
||||
return f"{base.rstrip('/')}{self.cryptopay_webhook_path}"
|
||||
return None
|
||||
|
||||
|
||||
@computed_field
|
||||
|
||||
Reference in New Issue
Block a user