fix: make provider services reactive to runtime config changes
This commit is contained in:
@@ -107,21 +107,40 @@ class CryptoPayService:
|
||||
self.async_session_factory = async_session_factory
|
||||
self.subscription_service = subscription_service
|
||||
self.referral_service = referral_service
|
||||
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:
|
||||
self._client = None
|
||||
self._client_token = None
|
||||
self._client_network = None
|
||||
if not self.config.TOKEN:
|
||||
logging.warning("CryptoPay token not provided. CryptoPay disabled")
|
||||
self.client = None
|
||||
self.configured = False
|
||||
|
||||
@property
|
||||
def token(self):
|
||||
return self.config.TOKEN
|
||||
|
||||
@property
|
||||
def configured(self) -> bool:
|
||||
return bool(self.config.ENABLED and self.config.TOKEN)
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
# Recreate the SDK client whenever the admin changes the token / network
|
||||
# at runtime — otherwise we'd keep talking to the old account.
|
||||
token = self.config.TOKEN
|
||||
network = self.config.NETWORK
|
||||
if not token:
|
||||
return None
|
||||
if self._client is None or token != self._client_token or network != self._client_network:
|
||||
net = Networks.TEST_NET if str(network).lower() == "testnet" else Networks.MAIN_NET
|
||||
self._client = AioCryptoPay(token=token, network=net)
|
||||
self._client.register_pay_handler(self._invoice_paid_handler)
|
||||
self._client_token = token
|
||||
self._client_network = network
|
||||
return self._client
|
||||
|
||||
async def close(self):
|
||||
if self.client:
|
||||
if self._client:
|
||||
try:
|
||||
await self.client.close()
|
||||
await self._client.close()
|
||||
logging.info("CryptoPay client session closed.")
|
||||
except Exception as e:
|
||||
logging.warning("Failed to close CryptoPay client: %s", e)
|
||||
|
||||
@@ -139,19 +139,13 @@ class FreeKassaService(HttpClientMixin):
|
||||
self.subscription_service = subscription_service
|
||||
self.referral_service = referral_service
|
||||
|
||||
self.shop_id: Optional[str] = config.MERCHANT_ID
|
||||
self.api_key: Optional[str] = config.API_KEY
|
||||
self.second_secret: Optional[str] = config.SECOND_SECRET
|
||||
self.default_currency: str = (settings.DEFAULT_CURRENCY_SYMBOL or "RUB").upper()
|
||||
self.server_ip: Optional[str] = config.PAYMENT_IP
|
||||
self.payment_method_id: Optional[int] = config.PAYMENT_METHOD_ID
|
||||
|
||||
self.api_base_url: str = "https://api.fk.life/v1"
|
||||
self._init_http_client(total_timeout=15)
|
||||
self._nonce_lock = asyncio.Lock()
|
||||
self._last_nonce = int(time.time() * 1000)
|
||||
|
||||
self.configured: bool = bool(config.ENABLED and self.shop_id and self.api_key)
|
||||
if not self.configured:
|
||||
logging.warning(
|
||||
"FreeKassaService initialized but not fully configured. Payments disabled."
|
||||
@@ -161,6 +155,30 @@ class FreeKassaService(HttpClientMixin):
|
||||
"FreeKassaService: FREEKASSA_PAYMENT_IP is not set. Requests may be rejected by the provider." # noqa: E501
|
||||
)
|
||||
|
||||
@property
|
||||
def configured(self) -> bool:
|
||||
return bool(self.config.ENABLED and self.shop_id and self.api_key)
|
||||
|
||||
@property
|
||||
def shop_id(self):
|
||||
return self.config.MERCHANT_ID
|
||||
|
||||
@property
|
||||
def api_key(self):
|
||||
return self.config.API_KEY
|
||||
|
||||
@property
|
||||
def second_secret(self):
|
||||
return self.config.SECOND_SECRET
|
||||
|
||||
@property
|
||||
def server_ip(self):
|
||||
return self.config.PAYMENT_IP
|
||||
|
||||
@property
|
||||
def payment_method_id(self):
|
||||
return self.config.PAYMENT_METHOD_ID
|
||||
|
||||
async def create_order(
|
||||
self,
|
||||
*,
|
||||
|
||||
@@ -171,27 +171,62 @@ class HeleketService(HttpClientMixin):
|
||||
self.async_session_factory = async_session_factory
|
||||
self.subscription_service = subscription_service
|
||||
self.referral_service = referral_service
|
||||
|
||||
self.base_url = (config.BASE_URL or "https://api.heleket.com").rstrip("/")
|
||||
self.merchant_id = config.MERCHANT_ID or ""
|
||||
self.api_key = config.API_KEY or ""
|
||||
self.currency = (config.CURRENCY or "RUB").upper()
|
||||
self.to_currency = (config.TO_CURRENCY or "").strip() or None
|
||||
self.network = (config.NETWORK or "").strip() or None
|
||||
self.return_url = config.RETURN_URL or f"https://t.me/{default_return_url}"
|
||||
self.success_url = config.SUCCESS_URL or self.return_url
|
||||
self.lifetime_seconds = config.LIFETIME_SECONDS
|
||||
self.verify_webhook_signature = config.VERIFY_WEBHOOK_SIGNATURE
|
||||
self._default_return_url = default_return_url
|
||||
|
||||
self._init_http_client(total_timeout=20)
|
||||
self.configured: bool = bool(
|
||||
config.ENABLED and self.merchant_id and self.api_key
|
||||
)
|
||||
if not self.configured:
|
||||
logging.warning(
|
||||
"HeleketService initialized but not fully configured. Payments disabled."
|
||||
)
|
||||
|
||||
# All of the following are properties on top of the live ``self.config``
|
||||
# so admin UI changes (which mutate the config bundle) take effect without
|
||||
# restarting the bot — otherwise ``configured`` would be frozen to the
|
||||
# ``False`` state from startup and the button would never appear.
|
||||
@property
|
||||
def configured(self) -> bool:
|
||||
return bool(self.config.ENABLED and self.merchant_id and self.api_key)
|
||||
|
||||
@property
|
||||
def base_url(self) -> str:
|
||||
return (self.config.BASE_URL or "https://api.heleket.com").rstrip("/")
|
||||
|
||||
@property
|
||||
def merchant_id(self) -> str:
|
||||
return self.config.MERCHANT_ID or ""
|
||||
|
||||
@property
|
||||
def api_key(self) -> str:
|
||||
return self.config.API_KEY or ""
|
||||
|
||||
@property
|
||||
def currency(self) -> str:
|
||||
return (self.config.CURRENCY or "RUB").upper()
|
||||
|
||||
@property
|
||||
def to_currency(self):
|
||||
return (self.config.TO_CURRENCY or "").strip() or None
|
||||
|
||||
@property
|
||||
def network(self):
|
||||
return (self.config.NETWORK or "").strip() or None
|
||||
|
||||
@property
|
||||
def return_url(self) -> str:
|
||||
return self.config.RETURN_URL or f"https://t.me/{self._default_return_url}"
|
||||
|
||||
@property
|
||||
def success_url(self) -> str:
|
||||
return self.config.SUCCESS_URL or self.return_url
|
||||
|
||||
@property
|
||||
def lifetime_seconds(self) -> int:
|
||||
return self.config.LIFETIME_SECONDS
|
||||
|
||||
@property
|
||||
def verify_webhook_signature(self) -> bool:
|
||||
return self.config.VERIFY_WEBHOOK_SIGNATURE
|
||||
|
||||
async def create_payment_link(
|
||||
self,
|
||||
*,
|
||||
|
||||
@@ -143,23 +143,9 @@ class PlategaService(HttpClientMixin):
|
||||
self.async_session_factory = async_session_factory
|
||||
self.subscription_service = subscription_service
|
||||
self.referral_service = referral_service
|
||||
|
||||
self.base_url = (config.BASE_URL or "https://app.platega.io").rstrip("/")
|
||||
self.merchant_id = config.MERCHANT_ID
|
||||
self.secret = config.SECRET
|
||||
self.payment_method = config.PAYMENT_METHOD
|
||||
self.sbp_method = config.sbp_method_resolved
|
||||
self.crypto_method = config.CRYPTO_METHOD
|
||||
self.return_url = config.RETURN_URL or f"https://t.me/{default_return_url}"
|
||||
self.failed_url = config.FAILED_URL or self.return_url
|
||||
self._default_return_url = default_return_url
|
||||
|
||||
self._init_http_client(total_timeout=20)
|
||||
self._auth_headers = {
|
||||
"X-MerchantId": self.merchant_id or "",
|
||||
"X-Secret": self.secret or "",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
self.configured: bool = bool(config.ENABLED and self.merchant_id and self.secret)
|
||||
if not self.configured:
|
||||
logging.warning(
|
||||
"PlategaService initialized but not fully configured. Payments disabled."
|
||||
@@ -173,6 +159,50 @@ class PlategaService(HttpClientMixin):
|
||||
self.crypto_method,
|
||||
)
|
||||
|
||||
@property
|
||||
def configured(self) -> bool:
|
||||
return bool(self.config.ENABLED and self.merchant_id and self.secret)
|
||||
|
||||
@property
|
||||
def base_url(self) -> str:
|
||||
return (self.config.BASE_URL or "https://app.platega.io").rstrip("/")
|
||||
|
||||
@property
|
||||
def merchant_id(self):
|
||||
return self.config.MERCHANT_ID
|
||||
|
||||
@property
|
||||
def secret(self):
|
||||
return self.config.SECRET
|
||||
|
||||
@property
|
||||
def payment_method(self) -> int:
|
||||
return self.config.PAYMENT_METHOD
|
||||
|
||||
@property
|
||||
def sbp_method(self) -> int:
|
||||
return self.config.sbp_method_resolved
|
||||
|
||||
@property
|
||||
def crypto_method(self) -> int:
|
||||
return self.config.CRYPTO_METHOD
|
||||
|
||||
@property
|
||||
def return_url(self) -> str:
|
||||
return self.config.RETURN_URL or f"https://t.me/{self._default_return_url}"
|
||||
|
||||
@property
|
||||
def failed_url(self) -> str:
|
||||
return self.config.FAILED_URL or self.return_url
|
||||
|
||||
@property
|
||||
def _auth_headers(self) -> dict:
|
||||
return {
|
||||
"X-MerchantId": self.merchant_id or "",
|
||||
"X-Secret": self.secret or "",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
async def create_transaction(
|
||||
self,
|
||||
*,
|
||||
|
||||
@@ -124,21 +124,39 @@ class SeverPayService(HttpClientMixin):
|
||||
self.async_session_factory = async_session_factory
|
||||
self.subscription_service = subscription_service
|
||||
self.referral_service = referral_service
|
||||
|
||||
self.base_url = (config.BASE_URL or "https://severpay.io/api/merchant").rstrip("/")
|
||||
self.mid = config.MID
|
||||
self.token = config.TOKEN or ""
|
||||
self.return_url = config.RETURN_URL or f"https://t.me/{default_return_url}"
|
||||
self.lifetime_minutes = config.LIFETIME_MINUTES
|
||||
self._default_return_url = default_return_url
|
||||
|
||||
self._init_http_client(total_timeout=15)
|
||||
|
||||
self.configured: bool = bool(config.ENABLED and self.mid and self.token)
|
||||
if not self.configured:
|
||||
logging.warning(
|
||||
"SeverPayService initialized but not fully configured. Payments disabled."
|
||||
)
|
||||
|
||||
@property
|
||||
def configured(self) -> bool:
|
||||
return bool(self.config.ENABLED and self.mid and self.token)
|
||||
|
||||
@property
|
||||
def base_url(self) -> str:
|
||||
return (self.config.BASE_URL or "https://severpay.io/api/merchant").rstrip("/")
|
||||
|
||||
@property
|
||||
def mid(self):
|
||||
return self.config.MID
|
||||
|
||||
@property
|
||||
def token(self) -> str:
|
||||
return self.config.TOKEN or ""
|
||||
|
||||
@property
|
||||
def return_url(self) -> str:
|
||||
return self.config.RETURN_URL or f"https://t.me/{self._default_return_url}"
|
||||
|
||||
@property
|
||||
def lifetime_minutes(self):
|
||||
return self.config.LIFETIME_MINUTES
|
||||
|
||||
@staticmethod
|
||||
def _format_amount(amount: float) -> str:
|
||||
return f"{format_decimal_amount(amount):.2f}"
|
||||
|
||||
@@ -138,20 +138,49 @@ class WataService(HttpClientMixin):
|
||||
self.async_session_factory = async_session_factory
|
||||
self.subscription_service = subscription_service
|
||||
self.referral_service = referral_service
|
||||
|
||||
self.base_url = (config.BASE_URL or "https://api.wata.pro/api/h2h").rstrip("/")
|
||||
self.api_token = config.API_TOKEN or ""
|
||||
self.return_url = config.RETURN_URL or f"https://t.me/{default_return_url}"
|
||||
self.failed_url = config.FAILED_URL or self.return_url
|
||||
self.payment_link_ttl_days = config.PAYMENT_LINK_TTL_DAYS
|
||||
self.verify_webhook_signature = config.WEBHOOK_VERIFY_SIGNATURE
|
||||
self._public_key_pem = config.PUBLIC_KEY
|
||||
self._default_return_url = default_return_url
|
||||
self._cached_public_key_pem = None # populated by webhook on first verify
|
||||
|
||||
self._init_http_client(total_timeout=20)
|
||||
self.configured: bool = bool(config.ENABLED and self.api_token)
|
||||
if not self.configured:
|
||||
logging.warning("WataService initialized but not fully configured. Payments disabled.")
|
||||
|
||||
@property
|
||||
def configured(self) -> bool:
|
||||
return bool(self.config.ENABLED and self.api_token)
|
||||
|
||||
@property
|
||||
def base_url(self) -> str:
|
||||
return (self.config.BASE_URL or "https://api.wata.pro/api/h2h").rstrip("/")
|
||||
|
||||
@property
|
||||
def api_token(self) -> str:
|
||||
return self.config.API_TOKEN or ""
|
||||
|
||||
@property
|
||||
def return_url(self) -> str:
|
||||
return self.config.RETURN_URL or f"https://t.me/{self._default_return_url}"
|
||||
|
||||
@property
|
||||
def failed_url(self) -> str:
|
||||
return self.config.FAILED_URL or self.return_url
|
||||
|
||||
@property
|
||||
def payment_link_ttl_days(self) -> int:
|
||||
return self.config.PAYMENT_LINK_TTL_DAYS
|
||||
|
||||
@property
|
||||
def verify_webhook_signature(self) -> bool:
|
||||
return self.config.WEBHOOK_VERIFY_SIGNATURE
|
||||
|
||||
@property
|
||||
def _public_key_pem(self):
|
||||
return self.config.PUBLIC_KEY or self._cached_public_key_pem
|
||||
|
||||
@_public_key_pem.setter
|
||||
def _public_key_pem(self, value):
|
||||
self._cached_public_key_pem = value
|
||||
|
||||
def _auth_headers(self) -> Dict[str, str]:
|
||||
return {
|
||||
"Authorization": f"Bearer {self.api_token}",
|
||||
|
||||
@@ -145,41 +145,54 @@ class YooKassaService:
|
||||
|
||||
self.settings = settings_obj
|
||||
self.config = config or YooKassaConfig()
|
||||
self._bot_username_for_default_return = bot_username_for_default_return
|
||||
self._configured_return_url_override = configured_return_url
|
||||
self._sdk_configured_for = None # (shop_id, secret_key) currently loaded into the global SDK
|
||||
|
||||
if not self.config.ENABLED:
|
||||
logging.warning(
|
||||
"YooKassa is disabled via YOOKASSA_ENABLED flag. Payment functionality will be DISABLED." # noqa: E501
|
||||
)
|
||||
self.configured = False
|
||||
elif not shop_id or not secret_key:
|
||||
logging.warning(
|
||||
"YooKassa SHOP_ID or SECRET_KEY not configured in settings. "
|
||||
"Payment functionality will be DISABLED."
|
||||
)
|
||||
self.configured = False
|
||||
else:
|
||||
try:
|
||||
Configuration.configure(shop_id, secret_key)
|
||||
self.configured = True
|
||||
logging.info(f"YooKassa SDK configured for shop_id: {shop_id[:5]}...")
|
||||
except Exception:
|
||||
logging.exception("Failed to configure YooKassa SDK.")
|
||||
self.configured = False
|
||||
if not self.configured:
|
||||
if not self.config.ENABLED:
|
||||
logging.warning(
|
||||
"YooKassa is disabled via YOOKASSA_ENABLED flag. Payment functionality will be DISABLED." # noqa: E501
|
||||
)
|
||||
else:
|
||||
logging.warning(
|
||||
"YooKassa SHOP_ID or SECRET_KEY not configured in settings. "
|
||||
"Payment functionality will be DISABLED."
|
||||
)
|
||||
logging.info("YooKassa Service effective return_url for payments: %s", self.return_url)
|
||||
|
||||
if configured_return_url:
|
||||
self.return_url = configured_return_url
|
||||
elif bot_username_for_default_return:
|
||||
self.return_url = f"https://t.me/{bot_username_for_default_return}"
|
||||
logging.info(
|
||||
f"YOOKASSA_RETURN_URL not set, using dynamic default based on bot username: {self.return_url}" # noqa: E501
|
||||
)
|
||||
else:
|
||||
self.return_url = "https://example.com/payment_error_no_return_url_configured"
|
||||
logging.warning(
|
||||
f"CRITICAL: YOOKASSA_RETURN_URL not set AND bot username not provided. "
|
||||
f"Using placeholder: {self.return_url}. Payments may not complete correctly."
|
||||
)
|
||||
logging.info(f"YooKassa Service effective return_url for payments: {self.return_url}")
|
||||
@property
|
||||
def configured(self) -> bool:
|
||||
if not (self.config.ENABLED and self.config.SHOP_ID and self.config.SECRET_KEY):
|
||||
return False
|
||||
self._ensure_sdk_configured()
|
||||
return self._sdk_configured_for is not None
|
||||
|
||||
def _ensure_sdk_configured(self) -> None:
|
||||
"""Reconfigure the global YooKassa SDK if shop_id/secret_key changed at runtime."""
|
||||
shop_id = self.config.SHOP_ID
|
||||
secret_key = self.config.SECRET_KEY
|
||||
if not shop_id or not secret_key:
|
||||
self._sdk_configured_for = None
|
||||
return
|
||||
if self._sdk_configured_for == (shop_id, secret_key):
|
||||
return
|
||||
try:
|
||||
Configuration.configure(shop_id, secret_key)
|
||||
self._sdk_configured_for = (shop_id, secret_key)
|
||||
logging.info("YooKassa SDK (re)configured for shop_id: %s...", shop_id[:5])
|
||||
except Exception:
|
||||
logging.exception("Failed to configure YooKassa SDK.")
|
||||
self._sdk_configured_for = None
|
||||
|
||||
@property
|
||||
def return_url(self) -> str:
|
||||
url = self._configured_return_url_override or self.config.RETURN_URL
|
||||
if url:
|
||||
return url
|
||||
if self._bot_username_for_default_return:
|
||||
return f"https://t.me/{self._bot_username_for_default_return}"
|
||||
return "https://example.com/payment_error_no_return_url_configured"
|
||||
|
||||
async def create_payment(
|
||||
self,
|
||||
|
||||
Reference in New Issue
Block a user