feat: add multicurrency tariff payments
This commit is contained in:
@@ -112,6 +112,7 @@ class WebAppPaymentContext:
|
||||
stars_price: Optional[int]
|
||||
description: str
|
||||
sale_mode: str
|
||||
currency: str = "RUB"
|
||||
traffic_gb: Optional[float] = None
|
||||
hwid_valid_from: Optional[Any] = None
|
||||
hwid_valid_until: Optional[Any] = None
|
||||
@@ -125,6 +126,36 @@ ServiceFactory = Callable[[ServiceFactoryContext], Any]
|
||||
WebhookPathGetter = Callable[[Any], str]
|
||||
WebhookRoute = Callable[[Any], Awaitable[Any]]
|
||||
WebAppPaymentFactory = Callable[[WebAppPaymentContext], Awaitable[Any]]
|
||||
CurrencySupportResolver = Callable[[Any], Optional[Sequence[str]]]
|
||||
|
||||
|
||||
def normalize_payment_currency_code(value: Any, default: str = "RUB") -> str:
|
||||
text = str(value or "").strip().upper()
|
||||
if not text:
|
||||
text = str(default).strip().upper() if default is not None else ""
|
||||
if not text:
|
||||
return ""
|
||||
aliases = {"RUR": "RUB", "STARS": "XTR", "STAR": "XTR"}
|
||||
normalized = aliases.get(text, text)
|
||||
return "".join(ch for ch in normalized if ch.isalnum() or ch in {"_", "-"}).strip("_-")
|
||||
|
||||
|
||||
def parse_supported_currency_codes(value: Any) -> tuple[str, ...]:
|
||||
if value is None:
|
||||
return ()
|
||||
if isinstance(value, str):
|
||||
raw_items = value.replace(";", ",").split(",")
|
||||
else:
|
||||
raw_items = list(value)
|
||||
currencies: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for item in raw_items:
|
||||
code = normalize_payment_currency_code(item, default="")
|
||||
if not code or code in seen:
|
||||
continue
|
||||
seen.add(code)
|
||||
currencies.append(code)
|
||||
return tuple(currencies)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -158,6 +189,10 @@ class PaymentProviderSpec:
|
||||
admin_only_manifest_key: Optional[str] = None
|
||||
admin_only_config_attr: str = "ADMIN_ONLY_ENABLED"
|
||||
admin_only_enabled: Optional[EnabledPredicate] = None
|
||||
supported_currencies: Optional[Sequence[str]] = ("RUB",)
|
||||
supported_currencies_resolver: Optional[CurrencySupportResolver] = None
|
||||
currency_support_note: str = ""
|
||||
currency_support_url: Optional[str] = None
|
||||
|
||||
@property
|
||||
def settings_key(self) -> str:
|
||||
@@ -237,6 +272,39 @@ class PaymentProviderSpec:
|
||||
service = app.get(self.service_key) if hasattr(app, "get") else None
|
||||
return bool(service and getattr(service, "configured", False))
|
||||
|
||||
def _currency_source(self, source: Any) -> Any:
|
||||
if self.config_class is not None and self.service_key:
|
||||
from .registry import get_provider_bundle
|
||||
|
||||
bundle = get_provider_bundle(self.service_key)
|
||||
if bundle and bundle.config is not None:
|
||||
return bundle.config
|
||||
return source
|
||||
|
||||
def supported_currency_codes(self, source: Any = None) -> Optional[tuple[str, ...]]:
|
||||
if self.price_source == "stars":
|
||||
return ("XTR",)
|
||||
source_for_currency = self._currency_source(source)
|
||||
if self.supported_currencies_resolver is not None:
|
||||
resolved = self.supported_currencies_resolver(source_for_currency)
|
||||
if resolved is None:
|
||||
return None
|
||||
return parse_supported_currency_codes(resolved)
|
||||
if self.supported_currencies is None:
|
||||
return None
|
||||
return parse_supported_currency_codes(self.supported_currencies)
|
||||
|
||||
def supports_currency(self, source: Any, currency: Any) -> bool:
|
||||
supported = self.supported_currency_codes(source)
|
||||
if supported is None:
|
||||
return True
|
||||
return normalize_payment_currency_code(currency) in supported
|
||||
|
||||
def is_usable_for_payment_currency(self, source: Any, currency: Any) -> bool:
|
||||
if self.price_source == "stars":
|
||||
return True
|
||||
return self.supports_currency(source, currency)
|
||||
|
||||
def is_visible(self, source: Any, app: Any) -> bool:
|
||||
return self.is_enabled(source) and self.is_service_configured(app)
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@ from bot.middlewares.i18n import JsonI18n
|
||||
from bot.services.referral_service import ReferralService
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
from config.settings import Settings
|
||||
from config.tariffs_config import (
|
||||
default_currency_key_for_settings,
|
||||
default_payment_currency_code_for_settings,
|
||||
)
|
||||
from db.dal import payment_dal
|
||||
|
||||
from .base import (
|
||||
@@ -25,6 +29,7 @@ from .base import (
|
||||
ProviderManifestField,
|
||||
ServiceFactoryContext,
|
||||
WebAppPaymentContext,
|
||||
normalize_payment_currency_code,
|
||||
provider_env_file,
|
||||
provider_runtime_enabled,
|
||||
)
|
||||
@@ -49,6 +54,34 @@ from .shared import (
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_LOG = "cryptopay"
|
||||
CRYPTOPAY_FIAT_CURRENCIES = (
|
||||
"USD",
|
||||
"EUR",
|
||||
"RUB",
|
||||
"BYN",
|
||||
"UAH",
|
||||
"GBP",
|
||||
"CNY",
|
||||
"KZT",
|
||||
"UZS",
|
||||
"GEL",
|
||||
"TRY",
|
||||
"AMD",
|
||||
"THB",
|
||||
"INR",
|
||||
"BRL",
|
||||
"IDR",
|
||||
"AZN",
|
||||
"AED",
|
||||
"PLN",
|
||||
"ILS",
|
||||
)
|
||||
CRYPTOPAY_CRYPTO_ASSETS = ("USDT", "TON", "BTC", "ETH", "LTC", "BNB", "TRX", "USDC")
|
||||
|
||||
|
||||
def _cryptopay_supported_currencies(config) -> tuple[str, ...]:
|
||||
currency_type = str(getattr(config, "CURRENCY_TYPE", "fiat") or "fiat").strip().lower()
|
||||
return CRYPTOPAY_CRYPTO_ASSETS if currency_type == "crypto" else CRYPTOPAY_FIAT_CURRENCIES
|
||||
|
||||
|
||||
class CryptoPayConfig(ProviderEnvConfig):
|
||||
@@ -159,11 +192,23 @@ class CryptoPayService:
|
||||
sale_mode: str = "subscription",
|
||||
url_kind: str = "bot",
|
||||
hwid_quote: Optional[dict] = None,
|
||||
currency: Optional[str] = None,
|
||||
) -> Optional[str]:
|
||||
if not self.configured or not self.client:
|
||||
logging.error("CryptoPayService not configured")
|
||||
return None
|
||||
|
||||
currency_code = normalize_payment_currency_code(currency or self.config.ASSET)
|
||||
currency_type = str(self.config.CURRENCY_TYPE or "fiat").strip().lower()
|
||||
supported = _cryptopay_supported_currencies(self.config)
|
||||
if currency_code not in supported:
|
||||
logging.error(
|
||||
"CryptoPay currency %s is not supported for currency_type=%s",
|
||||
currency_code,
|
||||
currency_type,
|
||||
)
|
||||
return None
|
||||
|
||||
sale_base = sale_mode_base(sale_mode)
|
||||
amounts = payment_record_amounts(months=months, sale_mode=sale_mode)
|
||||
try:
|
||||
@@ -172,7 +217,7 @@ class CryptoPayService:
|
||||
{
|
||||
"user_id": user_id,
|
||||
"amount": float(amount),
|
||||
"currency": self.config.ASSET,
|
||||
"currency": currency_code,
|
||||
"status": "pending_cryptopay",
|
||||
"description": description,
|
||||
"subscription_duration_months": (
|
||||
@@ -212,9 +257,9 @@ class CryptoPayService:
|
||||
try:
|
||||
invoice = await self.client.create_invoice(
|
||||
amount=amount,
|
||||
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,
|
||||
currency_type=currency_type,
|
||||
fiat=currency_code if currency_type == "fiat" else None,
|
||||
asset=currency_code if currency_type == "crypto" else None,
|
||||
description=description,
|
||||
payload=payload,
|
||||
)
|
||||
@@ -393,7 +438,7 @@ async def pay_crypto_callback_handler(
|
||||
user_id=callback.from_user.id,
|
||||
parts=parts,
|
||||
subscription_service=cryptopay_service.subscription_service,
|
||||
currency="rub",
|
||||
currency=default_currency_key_for_settings(settings),
|
||||
)
|
||||
if not parts:
|
||||
await notify_callback_parse_error(callback, translator)
|
||||
@@ -408,6 +453,7 @@ async def pay_crypto_callback_handler(
|
||||
description=payment_description,
|
||||
sale_mode=parts.sale_mode,
|
||||
hwid_quote=hwid_quote,
|
||||
currency=default_payment_currency_code_for_settings(settings),
|
||||
)
|
||||
|
||||
if invoice_url:
|
||||
@@ -457,6 +503,7 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
|
||||
description=ctx.description,
|
||||
sale_mode=ctx.sale_mode,
|
||||
url_kind="web",
|
||||
currency=ctx.currency,
|
||||
hwid_quote={
|
||||
"valid_from": ctx.hwid_valid_from,
|
||||
"valid_until": ctx.hwid_valid_until,
|
||||
@@ -592,4 +639,7 @@ SPEC = PaymentProviderSpec(
|
||||
config_class=CryptoPayConfig,
|
||||
presentation_class=CryptoPayPresentation,
|
||||
manifest_fields=_CONFIG_MANIFEST + _PRESENTATION_MANIFEST,
|
||||
supported_currencies_resolver=_cryptopay_supported_currencies,
|
||||
currency_support_note="Crypto Pay supports different sets for fiat invoices and crypto invoices; CURRENCY_TYPE selects which set is active.",
|
||||
currency_support_url="https://help.crypt.bot/crypto-pay-api/",
|
||||
)
|
||||
|
||||
@@ -20,6 +20,10 @@ from bot.services.referral_service import ReferralService
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
from bot.utils.request_security import ip_in_allowlist, request_client_ip
|
||||
from config.settings import Settings
|
||||
from config.tariffs_config import (
|
||||
default_currency_key_for_settings,
|
||||
default_payment_currency_code_for_settings,
|
||||
)
|
||||
from db.dal import payment_dal
|
||||
|
||||
from .base import (
|
||||
@@ -28,6 +32,7 @@ from .base import (
|
||||
ProviderManifestField,
|
||||
ServiceFactoryContext,
|
||||
WebAppPaymentContext,
|
||||
normalize_payment_currency_code,
|
||||
provider_env_file,
|
||||
provider_runtime_enabled,
|
||||
)
|
||||
@@ -56,6 +61,7 @@ from .shared import (
|
||||
)
|
||||
|
||||
_LOG = "freekassa"
|
||||
FREEKASSA_SUPPORTED_CURRENCIES = ("RUB", "USD", "EUR", "UAH", "KZT")
|
||||
|
||||
|
||||
class FreeKassaConfig(ProviderEnvConfig):
|
||||
@@ -144,7 +150,7 @@ class FreeKassaService(HttpClientMixin):
|
||||
self.subscription_service = subscription_service
|
||||
self.referral_service = referral_service
|
||||
|
||||
self.default_currency: str = (settings.DEFAULT_CURRENCY_SYMBOL or "RUB").upper()
|
||||
self.default_currency: str = default_payment_currency_code_for_settings(settings).upper()
|
||||
|
||||
self.api_base_url: str = "https://api.fk.life/v1"
|
||||
self._init_http_client(total_timeout=15)
|
||||
@@ -207,7 +213,13 @@ class FreeKassaService(HttpClientMixin):
|
||||
return False, {"message": "missing_ip"}
|
||||
|
||||
email = email or f"{user_id}@telegram.org"
|
||||
currency_code = (currency or self.default_currency or "RUB").upper()
|
||||
currency_code = normalize_payment_currency_code(currency or self.default_currency or "RUB")
|
||||
if currency_code not in FREEKASSA_SUPPORTED_CURRENCIES:
|
||||
return False, {
|
||||
"message": "unsupported_currency",
|
||||
"currency": currency_code,
|
||||
"supported_currencies": list(FREEKASSA_SUPPORTED_CURRENCIES),
|
||||
}
|
||||
|
||||
payload: Dict[str, Any] = {
|
||||
"shopId": int(self.shop_id),
|
||||
@@ -477,7 +489,7 @@ async def pay_fk_callback_handler(
|
||||
user_id=callback.from_user.id,
|
||||
parts=parts,
|
||||
subscription_service=freekassa_service.subscription_service,
|
||||
currency="rub",
|
||||
currency=default_currency_key_for_settings(settings),
|
||||
)
|
||||
if not parts:
|
||||
await notify_callback_parse_error(callback, translator)
|
||||
@@ -485,7 +497,7 @@ async def pay_fk_callback_handler(
|
||||
|
||||
currency_code = (
|
||||
getattr(freekassa_service, "default_currency", None)
|
||||
or settings.DEFAULT_CURRENCY_SYMBOL
|
||||
or default_payment_currency_code_for_settings(settings)
|
||||
or "RUB"
|
||||
)
|
||||
payment_description = describe_payment(translator, parts)
|
||||
@@ -578,12 +590,13 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
|
||||
service: FreeKassaService = ctx.request.app["freekassa_service"]
|
||||
if not service or not service.configured or not service.payment_method_id:
|
||||
return payment_unavailable()
|
||||
currency = ctx.currency or service.default_currency
|
||||
|
||||
try:
|
||||
payment = await create_webapp_payment_record(
|
||||
ctx,
|
||||
amount=ctx.price,
|
||||
currency=service.default_currency,
|
||||
currency=currency,
|
||||
status="pending_freekassa",
|
||||
provider="freekassa",
|
||||
)
|
||||
@@ -592,7 +605,7 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
|
||||
user_id=ctx.user_id,
|
||||
months=ctx.months,
|
||||
amount=ctx.price,
|
||||
currency=service.default_currency,
|
||||
currency=currency,
|
||||
payment_method_id=service.payment_method_id,
|
||||
ip_address=service.server_ip,
|
||||
extra_params={"us_method": service.payment_method_id},
|
||||
@@ -762,4 +775,7 @@ SPEC = PaymentProviderSpec(
|
||||
config_class=FreeKassaConfig,
|
||||
presentation_class=FreeKassaPresentation,
|
||||
manifest_fields=_CONFIG_MANIFEST + _PRESENTATION_MANIFEST,
|
||||
supported_currencies=FREEKASSA_SUPPORTED_CURRENCIES,
|
||||
currency_support_note="FreeKassa SCI documents the payment currency parameter as RUB, USD, EUR, UAH or KZT.",
|
||||
currency_support_url="https://docs.freekassa.net/",
|
||||
)
|
||||
|
||||
@@ -18,6 +18,10 @@ from bot.services.referral_service import ReferralService
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
from bot.utils.request_security import ip_in_allowlist, request_client_ip
|
||||
from config.settings import Settings
|
||||
from config.tariffs_config import (
|
||||
default_currency_key_for_settings,
|
||||
default_payment_currency_code_for_settings,
|
||||
)
|
||||
from db.dal import payment_dal
|
||||
|
||||
from .base import (
|
||||
@@ -26,6 +30,8 @@ from .base import (
|
||||
ProviderManifestField,
|
||||
ServiceFactoryContext,
|
||||
WebAppPaymentContext,
|
||||
normalize_payment_currency_code,
|
||||
parse_supported_currency_codes,
|
||||
provider_env_file,
|
||||
provider_runtime_enabled,
|
||||
)
|
||||
@@ -59,6 +65,10 @@ _LOG = "heleket"
|
||||
|
||||
_SUCCESS_STATUSES = {"paid", "paid_over"}
|
||||
_FAILED_STATUSES = {"fail", "wrong_amount", "cancel", "system_fail"}
|
||||
HELEKET_DEFAULT_SUPPORTED_CURRENCIES = (
|
||||
"RUB,USD,EUR,USDT,USDC,BTC,ETH,LTC,TON,TRX,BNB,BCH,DASH,DAI,DOGE,"
|
||||
"MATIC,SHIB,SOL,XMR,AVAX,BUSD,VERSE"
|
||||
)
|
||||
|
||||
|
||||
class HeleketConfig(ProviderEnvConfig):
|
||||
@@ -83,6 +93,7 @@ class HeleketConfig(ProviderEnvConfig):
|
||||
LIFETIME_SECONDS: int = Field(default=3600)
|
||||
VERIFY_WEBHOOK_SIGNATURE: bool = Field(default=True)
|
||||
TRUSTED_IPS: str = Field(default="31.133.220.8")
|
||||
SUPPORTED_CURRENCIES: str = Field(default=HELEKET_DEFAULT_SUPPORTED_CURRENCIES)
|
||||
|
||||
@field_validator("LIFETIME_SECONDS", mode="before")
|
||||
@classmethod
|
||||
@@ -299,9 +310,18 @@ class HeleketService(HttpClientMixin):
|
||||
logging.error("HeleketService is not configured. Cannot create payment link.")
|
||||
return False, {"message": "service_not_configured"}
|
||||
|
||||
currency_code = normalize_payment_currency_code(currency or self.currency)
|
||||
supported = parse_supported_currency_codes(self.config.SUPPORTED_CURRENCIES)
|
||||
if supported and currency_code not in supported:
|
||||
return False, {
|
||||
"message": "unsupported_currency",
|
||||
"currency": currency_code,
|
||||
"supported_currencies": list(supported),
|
||||
}
|
||||
|
||||
body: Dict[str, Any] = {
|
||||
"amount": str(format_decimal_amount(amount)),
|
||||
"currency": (currency or self.currency).upper(),
|
||||
"currency": currency_code,
|
||||
"order_id": str(payment_db_id),
|
||||
"url_return": self.return_url,
|
||||
"url_success": self.success_url,
|
||||
@@ -572,13 +592,13 @@ async def pay_heleket_callback_handler(
|
||||
user_id=callback.from_user.id,
|
||||
parts=parts,
|
||||
subscription_service=heleket_service.subscription_service,
|
||||
currency="rub",
|
||||
currency=default_currency_key_for_settings(settings),
|
||||
)
|
||||
if not parts:
|
||||
await notify_callback_parse_error(callback, translator)
|
||||
return
|
||||
|
||||
currency_code = (heleket_service.currency or settings.DEFAULT_CURRENCY_SYMBOL or "RUB").upper()
|
||||
currency_code = default_payment_currency_code_for_settings(settings)
|
||||
payment_description = describe_payment(translator, parts)
|
||||
record_payload = build_payment_record_payload(
|
||||
user_id=callback.from_user.id,
|
||||
@@ -632,7 +652,7 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
|
||||
if not service or not service.configured:
|
||||
return payment_unavailable()
|
||||
|
||||
currency = (service.currency or settings.DEFAULT_CURRENCY_SYMBOL or "RUB").upper()
|
||||
currency = ctx.currency or default_payment_currency_code_for_settings(settings)
|
||||
try:
|
||||
payment = await create_webapp_payment_record(
|
||||
ctx,
|
||||
@@ -787,6 +807,18 @@ _CONFIG_MANIFEST = (
|
||||
subsection="Heleket",
|
||||
attr="CURRENCY",
|
||||
),
|
||||
ProviderManifestField(
|
||||
"HELEKET_SUPPORTED_CURRENCIES",
|
||||
"string",
|
||||
"Supported currencies",
|
||||
description=(
|
||||
"Comma-separated invoice currencies allowed for Heleket in this shop. "
|
||||
"Heleket can reject unsupported codes per account/service."
|
||||
),
|
||||
placeholder=HELEKET_DEFAULT_SUPPORTED_CURRENCIES,
|
||||
subsection="Heleket",
|
||||
attr="SUPPORTED_CURRENCIES",
|
||||
),
|
||||
ProviderManifestField(
|
||||
"HELEKET_TO_CURRENCY",
|
||||
"string",
|
||||
@@ -859,4 +891,9 @@ SPEC = PaymentProviderSpec(
|
||||
config_class=HeleketConfig,
|
||||
presentation_class=HeleketPresentation,
|
||||
manifest_fields=_CONFIG_MANIFEST + _PRESENTATION_MANIFEST,
|
||||
supported_currencies_resolver=lambda config: getattr(
|
||||
config, "SUPPORTED_CURRENCIES", HELEKET_DEFAULT_SUPPORTED_CURRENCIES
|
||||
),
|
||||
currency_support_note="Heleket supports crypto and fiat invoice currencies, but exact availability can depend on service/account settings.",
|
||||
currency_support_url="https://doc.heleket.com/methods/payments/creating-invoice",
|
||||
)
|
||||
|
||||
@@ -14,6 +14,10 @@ from bot.middlewares.i18n import JsonI18n
|
||||
from bot.services.referral_service import ReferralService
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
from config.settings import Settings
|
||||
from config.tariffs_config import (
|
||||
default_currency_key_for_settings,
|
||||
default_payment_currency_code_for_settings,
|
||||
)
|
||||
from db.dal import payment_dal
|
||||
|
||||
from .base import (
|
||||
@@ -22,6 +26,8 @@ from .base import (
|
||||
ProviderManifestField,
|
||||
ServiceFactoryContext,
|
||||
WebAppPaymentContext,
|
||||
normalize_payment_currency_code,
|
||||
parse_supported_currency_codes,
|
||||
provider_env_file,
|
||||
provider_runtime_enabled,
|
||||
)
|
||||
@@ -76,6 +82,7 @@ class PlategaConfig(ProviderEnvConfig):
|
||||
CRYPTO_METHOD: int = Field(default=13)
|
||||
RETURN_URL: Optional[str] = None
|
||||
FAILED_URL: Optional[str] = None
|
||||
SUPPORTED_CURRENCIES: str = Field(default="RUB")
|
||||
|
||||
@field_validator("MERCHANT_ID", "SECRET", "RETURN_URL", "FAILED_URL", mode="before")
|
||||
@classmethod
|
||||
@@ -229,9 +236,19 @@ class PlategaService(HttpClientMixin):
|
||||
logging.error("PlategaService is not configured. Cannot create transaction.")
|
||||
return False, {"message": "service_not_configured"}
|
||||
|
||||
currency_code = normalize_payment_currency_code(
|
||||
currency or self.settings.DEFAULT_CURRENCY_SYMBOL or "RUB"
|
||||
)
|
||||
supported = parse_supported_currency_codes(self.config.SUPPORTED_CURRENCIES)
|
||||
if supported and currency_code not in supported:
|
||||
return False, {
|
||||
"message": "unsupported_currency",
|
||||
"currency": currency_code,
|
||||
"supported_currencies": list(supported),
|
||||
}
|
||||
|
||||
session = await self._get_session()
|
||||
url = f"{self.base_url}/transaction/process"
|
||||
currency_code = (currency or self.settings.DEFAULT_CURRENCY_SYMBOL or "RUB").upper()
|
||||
method_id = int(payment_method if payment_method is not None else self.payment_method)
|
||||
|
||||
body: Dict[str, Any] = {
|
||||
@@ -482,13 +499,13 @@ async def pay_platega_callback_handler(
|
||||
user_id=callback.from_user.id,
|
||||
parts=parts,
|
||||
subscription_service=platega_service.subscription_service,
|
||||
currency="rub",
|
||||
currency=default_currency_key_for_settings(settings),
|
||||
)
|
||||
if not parts:
|
||||
await notify_callback_parse_error(callback, translator)
|
||||
return
|
||||
|
||||
currency_code = settings.DEFAULT_CURRENCY_SYMBOL or "RUB"
|
||||
currency_code = default_payment_currency_code_for_settings(settings)
|
||||
payment_description = describe_payment(translator, parts)
|
||||
record_payload = build_payment_record_payload(
|
||||
user_id=callback.from_user.id,
|
||||
@@ -596,7 +613,7 @@ async def _create_webapp_payment(ctx: WebAppPaymentContext, variant: str) -> web
|
||||
payment = await create_webapp_payment_record(
|
||||
ctx,
|
||||
amount=ctx.price,
|
||||
currency=settings.DEFAULT_CURRENCY_SYMBOL or "RUB",
|
||||
currency=ctx.currency or settings.DEFAULT_CURRENCY_SYMBOL or "RUB",
|
||||
status="pending_platega",
|
||||
provider="platega",
|
||||
)
|
||||
@@ -616,7 +633,7 @@ async def _create_webapp_payment(ctx: WebAppPaymentContext, variant: str) -> web
|
||||
)
|
||||
success, response_data = await service.create_transaction(
|
||||
amount=ctx.price,
|
||||
currency=settings.DEFAULT_CURRENCY_SYMBOL or "RUB",
|
||||
currency=ctx.currency or settings.DEFAULT_CURRENCY_SYMBOL or "RUB",
|
||||
description=ctx.description,
|
||||
payload=payload,
|
||||
payment_method=platega_method_id,
|
||||
@@ -757,6 +774,18 @@ _CONFIG_MANIFEST = (
|
||||
subsection="Platega",
|
||||
attr="CRYPTO_METHOD",
|
||||
),
|
||||
ProviderManifestField(
|
||||
"PLATEGA_SUPPORTED_CURRENCIES",
|
||||
"string",
|
||||
"Supported currencies",
|
||||
description=(
|
||||
"Comma-separated payment currencies enabled for your Platega merchant. "
|
||||
"Public docs expose currency per method/limits but do not publish a fixed global list."
|
||||
),
|
||||
placeholder="RUB",
|
||||
subsection="Platega",
|
||||
attr="SUPPORTED_CURRENCIES",
|
||||
),
|
||||
ProviderManifestField(
|
||||
"PLATEGA_RETURN_URL", "url", "Return URL", subsection="Platega", attr="RETURN_URL"
|
||||
),
|
||||
@@ -793,6 +822,9 @@ SBP_SPEC = PaymentProviderSpec(
|
||||
presentation_class=PlategaSbpPresentation,
|
||||
manifest_fields=_CONFIG_MANIFEST
|
||||
+ _platega_presentation_manifest("Platega", "CreditCard", "PLATEGA_SBP"),
|
||||
supported_currencies_resolver=lambda config: getattr(config, "SUPPORTED_CURRENCIES", "RUB"),
|
||||
currency_support_note="Platega currencies are merchant/method-specific; configure the codes enabled for your account.",
|
||||
currency_support_url="https://docs.platega.io/",
|
||||
)
|
||||
|
||||
CRYPTO_SPEC = PaymentProviderSpec(
|
||||
@@ -818,6 +850,9 @@ CRYPTO_SPEC = PaymentProviderSpec(
|
||||
config_class=PlategaConfig,
|
||||
presentation_class=PlategaCryptoPresentation,
|
||||
manifest_fields=_platega_presentation_manifest("Platega", "Bitcoin", "PLATEGA_CRYPTO"),
|
||||
supported_currencies_resolver=lambda config: getattr(config, "SUPPORTED_CURRENCIES", "RUB"),
|
||||
currency_support_note="Platega currencies are merchant/method-specific; configure the codes enabled for your account.",
|
||||
currency_support_url="https://docs.platega.io/",
|
||||
)
|
||||
|
||||
SPECS = (SBP_SPEC, CRYPTO_SPEC)
|
||||
|
||||
@@ -16,6 +16,10 @@ from bot.middlewares.i18n import JsonI18n
|
||||
from bot.services.referral_service import ReferralService
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
from config.settings import Settings
|
||||
from config.tariffs_config import (
|
||||
default_currency_key_for_settings,
|
||||
default_payment_currency_code_for_settings,
|
||||
)
|
||||
from db.dal import payment_dal
|
||||
|
||||
from .base import (
|
||||
@@ -24,6 +28,8 @@ from .base import (
|
||||
ProviderManifestField,
|
||||
ServiceFactoryContext,
|
||||
WebAppPaymentContext,
|
||||
normalize_payment_currency_code,
|
||||
parse_supported_currency_codes,
|
||||
provider_env_file,
|
||||
provider_runtime_enabled,
|
||||
)
|
||||
@@ -69,6 +75,7 @@ class SeverPayConfig(ProviderEnvConfig):
|
||||
RETURN_URL: Optional[str] = None
|
||||
BASE_URL: str = Field(default="https://severpay.io/api/merchant")
|
||||
LIFETIME_MINUTES: Optional[int] = None
|
||||
SUPPORTED_CURRENCIES: str = Field(default="RUB,USD")
|
||||
|
||||
@field_validator("MID", "LIFETIME_MINUTES", mode="before")
|
||||
@classmethod
|
||||
@@ -201,9 +208,19 @@ class SeverPayService(HttpClientMixin):
|
||||
logging.error("SeverPayService is not configured. Cannot create payment.")
|
||||
return False, {"message": "service_not_configured"}
|
||||
|
||||
currency_code = normalize_payment_currency_code(
|
||||
currency or self.settings.DEFAULT_CURRENCY_SYMBOL or "RUB"
|
||||
)
|
||||
supported = parse_supported_currency_codes(self.config.SUPPORTED_CURRENCIES)
|
||||
if supported and currency_code not in supported:
|
||||
return False, {
|
||||
"message": "unsupported_currency",
|
||||
"currency": currency_code,
|
||||
"supported_currencies": list(supported),
|
||||
}
|
||||
|
||||
session = await self._get_session()
|
||||
url = f"{self.base_url}/payin/create"
|
||||
currency_code = (currency or self.settings.DEFAULT_CURRENCY_SYMBOL or "RUB").upper()
|
||||
|
||||
body = {
|
||||
"order_id": str(payment_db_id),
|
||||
@@ -428,13 +445,13 @@ async def pay_severpay_callback_handler(
|
||||
user_id=callback.from_user.id,
|
||||
parts=parts,
|
||||
subscription_service=severpay_service.subscription_service,
|
||||
currency="rub",
|
||||
currency=default_currency_key_for_settings(settings),
|
||||
)
|
||||
if not parts:
|
||||
await notify_callback_parse_error(callback, translator)
|
||||
return
|
||||
|
||||
currency_code = settings.DEFAULT_CURRENCY_SYMBOL or "RUB"
|
||||
currency_code = default_payment_currency_code_for_settings(settings)
|
||||
payment_description = describe_payment(translator, parts)
|
||||
record_payload = build_payment_record_payload(
|
||||
user_id=callback.from_user.id,
|
||||
@@ -503,7 +520,7 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
|
||||
if not service or not service.configured:
|
||||
return payment_unavailable()
|
||||
|
||||
currency = settings.DEFAULT_CURRENCY_SYMBOL or "RUB"
|
||||
currency = ctx.currency or settings.DEFAULT_CURRENCY_SYMBOL or "RUB"
|
||||
try:
|
||||
payment = await create_webapp_payment_record(
|
||||
ctx,
|
||||
@@ -627,6 +644,18 @@ _CONFIG_MANIFEST = (
|
||||
max=4320,
|
||||
attr="LIFETIME_MINUTES",
|
||||
),
|
||||
ProviderManifestField(
|
||||
"SEVERPAY_SUPPORTED_CURRENCIES",
|
||||
"string",
|
||||
"Supported currencies",
|
||||
description=(
|
||||
"Comma-separated currencies enabled for your SeverPay merchant. "
|
||||
"The public PayIn docs show USD examples but do not publish a fixed global list."
|
||||
),
|
||||
placeholder="RUB,USD",
|
||||
subsection="SeverPay",
|
||||
attr="SUPPORTED_CURRENCIES",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -651,4 +680,7 @@ SPEC = PaymentProviderSpec(
|
||||
config_class=SeverPayConfig,
|
||||
presentation_class=SeverPayPresentation,
|
||||
manifest_fields=_CONFIG_MANIFEST + _PRESENTATION_MANIFEST,
|
||||
supported_currencies_resolver=lambda config: getattr(config, "SUPPORTED_CURRENCIES", "RUB,USD"),
|
||||
currency_support_note="SeverPay PayIn requires a currency; keep this list aligned with your merchant account.",
|
||||
currency_support_url="https://docs.severpay.io/ru/payin/create",
|
||||
)
|
||||
|
||||
@@ -484,4 +484,6 @@ SPEC = PaymentProviderSpec(
|
||||
telegram_emoji="⭐",
|
||||
presentation_class=StarsPresentation,
|
||||
manifest_fields=_PRESENTATION_MANIFEST,
|
||||
supported_currencies=("XTR",),
|
||||
currency_support_note="Telegram Stars use Telegram's XTR currency and separate Stars prices.",
|
||||
)
|
||||
|
||||
@@ -19,6 +19,10 @@ from bot.services.referral_service import ReferralService
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
from bot.utils.request_security import ip_in_allowlist, request_client_ip
|
||||
from config.settings import Settings
|
||||
from config.tariffs_config import (
|
||||
default_currency_key_for_settings,
|
||||
default_payment_currency_code_for_settings,
|
||||
)
|
||||
from db.dal import payment_dal
|
||||
|
||||
from .base import (
|
||||
@@ -27,6 +31,7 @@ from .base import (
|
||||
ProviderManifestField,
|
||||
ServiceFactoryContext,
|
||||
WebAppPaymentContext,
|
||||
normalize_payment_currency_code,
|
||||
provider_env_file,
|
||||
provider_runtime_enabled,
|
||||
)
|
||||
@@ -63,6 +68,7 @@ from .shared import (
|
||||
|
||||
router = Router(name="user_subscription_payments_wata_router")
|
||||
_LOG = "wata"
|
||||
WATA_SUPPORTED_CURRENCIES = ("RUB", "USD", "EUR")
|
||||
_WATA_IN_PROGRESS_STATUSES = {"created", "pending"}
|
||||
_WATA_LINK_OPENED_STATUSES = {"opened", "open"}
|
||||
_WATA_LINK_DEFAULT_TTL_MINUTES = 15
|
||||
@@ -258,13 +264,23 @@ class WataService(HttpClientMixin):
|
||||
logging.error("WataService is not configured. Cannot create payment link.")
|
||||
return False, {"message": "service_not_configured"}
|
||||
|
||||
currency_code = normalize_payment_currency_code(
|
||||
currency or self.settings.DEFAULT_CURRENCY_SYMBOL or "RUB"
|
||||
)
|
||||
if currency_code not in WATA_SUPPORTED_CURRENCIES:
|
||||
return False, {
|
||||
"message": "unsupported_currency",
|
||||
"currency": currency_code,
|
||||
"supported_currencies": list(WATA_SUPPORTED_CURRENCIES),
|
||||
}
|
||||
|
||||
session = await self._get_session()
|
||||
expires_at = (
|
||||
datetime.now(timezone.utc) + timedelta(minutes=self.payment_link_ttl_minutes)
|
||||
).replace(microsecond=0)
|
||||
body: Dict[str, Any] = {
|
||||
"amount": float(format_decimal_amount(amount)),
|
||||
"currency": (currency or self.settings.DEFAULT_CURRENCY_SYMBOL or "RUB").upper(),
|
||||
"currency": currency_code,
|
||||
"description": description,
|
||||
"orderId": str(payment_db_id),
|
||||
"successRedirectUrl": self.return_url,
|
||||
@@ -863,13 +879,13 @@ async def pay_wata_callback_handler(
|
||||
user_id=callback.from_user.id,
|
||||
parts=parts,
|
||||
subscription_service=wata_service.subscription_service,
|
||||
currency="rub",
|
||||
currency=default_currency_key_for_settings(settings),
|
||||
)
|
||||
if not parts:
|
||||
await notify_callback_parse_error(callback, translator)
|
||||
return
|
||||
|
||||
currency_code = settings.DEFAULT_CURRENCY_SYMBOL or "RUB"
|
||||
currency_code = default_payment_currency_code_for_settings(settings)
|
||||
payment_description = describe_payment(translator, parts)
|
||||
|
||||
reuse_amounts = payment_record_amounts(months=parts.months, sale_mode=parts.sale_mode)
|
||||
@@ -956,7 +972,7 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
|
||||
if not service or not service.configured:
|
||||
return payment_unavailable()
|
||||
|
||||
currency = settings.DEFAULT_CURRENCY_SYMBOL or "RUB"
|
||||
currency = ctx.currency or settings.DEFAULT_CURRENCY_SYMBOL or "RUB"
|
||||
|
||||
reuse_amounts = payment_record_amounts(
|
||||
months=ctx.months,
|
||||
@@ -1190,4 +1206,7 @@ SPEC = PaymentProviderSpec(
|
||||
config_class=WataConfig,
|
||||
presentation_class=WataPresentation,
|
||||
manifest_fields=_CONFIG_MANIFEST + _PRESENTATION_MANIFEST,
|
||||
supported_currencies=WATA_SUPPORTED_CURRENCIES,
|
||||
currency_support_note="WATA H2H payment links and widget document RUB, USD and EUR as payment currencies.",
|
||||
currency_support_url="https://wata.pro/api",
|
||||
)
|
||||
|
||||
@@ -40,6 +40,10 @@ from bot.utils.config_link import prepare_config_links
|
||||
from bot.utils.install_links import ensure_user_install_guide_links
|
||||
from bot.utils.request_security import ip_in_allowlist, request_client_ip
|
||||
from config.settings import Settings
|
||||
from config.tariffs_config import (
|
||||
default_currency_key_for_settings,
|
||||
default_payment_currency_code_for_settings,
|
||||
)
|
||||
from db.dal import payment_dal, user_billing_dal, user_dal
|
||||
from db.models import Payment
|
||||
|
||||
@@ -49,6 +53,7 @@ from .base import (
|
||||
ProviderManifestField,
|
||||
ServiceFactoryContext,
|
||||
WebAppPaymentContext,
|
||||
normalize_payment_currency_code,
|
||||
provider_env_file,
|
||||
provider_runtime_enabled,
|
||||
)
|
||||
@@ -234,6 +239,11 @@ class YooKassaService:
|
||||
"internal_message": "Service settings (Settings object) not initialized.",
|
||||
}
|
||||
|
||||
currency = normalize_payment_currency_code(currency)
|
||||
if currency != "RUB":
|
||||
logging.error("YooKassa currency %s is not supported by this integration", currency)
|
||||
return None
|
||||
|
||||
customer_contact_for_receipt = {}
|
||||
if receipt_email:
|
||||
customer_contact_for_receipt["email"] = receipt_email
|
||||
@@ -885,7 +895,7 @@ async def process_successful_payment(
|
||||
i18n=i18n,
|
||||
user_id=user_id,
|
||||
amount=payment_value,
|
||||
currency=settings.DEFAULT_CURRENCY_SYMBOL,
|
||||
currency=amount_data.get("currency", default_payment_currency_code_for_settings(settings)),
|
||||
months_for_admin=int(subscription_months) if sale_mode_base == "subscription" else 0,
|
||||
traffic_gb_for_admin=(
|
||||
traffic_amount_gb if is_traffic_sale_base(sale_mode_base) else None
|
||||
@@ -1702,7 +1712,7 @@ async def pay_yk_callback_handler(
|
||||
user_id=callback.from_user.id,
|
||||
parts=PaymentCallbackParts(months=months, price=price_rub, sale_mode=sale_mode),
|
||||
subscription_service=yookassa_service.subscription_service,
|
||||
currency="rub",
|
||||
currency=default_currency_key_for_settings(settings),
|
||||
)
|
||||
if not quoted_parts:
|
||||
try:
|
||||
@@ -1713,7 +1723,7 @@ async def pay_yk_callback_handler(
|
||||
months = quoted_parts.months
|
||||
price_rub = quoted_parts.price
|
||||
user_id = callback.from_user.id
|
||||
currency_code_for_yk = "RUB"
|
||||
currency_code_for_yk = default_payment_currency_code_for_settings(settings)
|
||||
autopay_enabled = bool(
|
||||
settings.yookassa_autopayments_active
|
||||
and _sale_mode_base(sale_mode) == "subscription"
|
||||
@@ -1851,7 +1861,7 @@ async def pay_yk_new_card_handler(
|
||||
|
||||
months, price_rub, sale_mode = parsed
|
||||
user_id = callback.from_user.id
|
||||
currency_code_for_yk = "RUB"
|
||||
currency_code_for_yk = default_payment_currency_code_for_settings(settings)
|
||||
autopay_enabled = bool(
|
||||
settings.yookassa_autopayments_active
|
||||
and _sale_mode_base(sale_mode) == "subscription"
|
||||
@@ -2154,7 +2164,7 @@ async def pay_yk_use_saved_handler(
|
||||
pass
|
||||
return
|
||||
|
||||
currency_code_for_yk = "RUB"
|
||||
currency_code_for_yk = default_payment_currency_code_for_settings(settings)
|
||||
|
||||
await _initiate_yk_payment(
|
||||
callback,
|
||||
@@ -2260,7 +2270,7 @@ async def payment_method_bind(
|
||||
metadata = {"user_id": str(callback.from_user.id), "bind_only": "1"}
|
||||
resp = await yookassa_service.create_payment(
|
||||
amount=1.00,
|
||||
currency="RUB",
|
||||
currency=default_payment_currency_code_for_settings(settings),
|
||||
description="Bind card",
|
||||
metadata=metadata,
|
||||
receipt_email=yookassa_service.config.DEFAULT_RECEIPT_EMAIL,
|
||||
@@ -2734,6 +2744,7 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
|
||||
service: YooKassaService = ctx.request.app["yookassa_service"]
|
||||
if not service or not service.configured:
|
||||
return payment_unavailable()
|
||||
currency = (ctx.currency or "RUB").upper()
|
||||
|
||||
try:
|
||||
amounts = payment_record_amounts(
|
||||
@@ -2744,7 +2755,7 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
|
||||
payment = await create_webapp_payment_record(
|
||||
ctx,
|
||||
amount=ctx.price,
|
||||
currency="RUB",
|
||||
currency=currency,
|
||||
status="pending_yookassa",
|
||||
provider="yookassa",
|
||||
)
|
||||
@@ -2767,7 +2778,7 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
|
||||
metadata["tariff_key"] = amounts.tariff_key
|
||||
response = await service.create_payment(
|
||||
amount=ctx.price,
|
||||
currency="RUB",
|
||||
currency=currency,
|
||||
description=ctx.description,
|
||||
metadata=metadata,
|
||||
receipt_email=service.config.DEFAULT_RECEIPT_EMAIL,
|
||||
@@ -2929,4 +2940,7 @@ SPEC = PaymentProviderSpec(
|
||||
config_class=YooKassaConfig,
|
||||
presentation_class=YooKassaPresentation,
|
||||
manifest_fields=_CONFIG_MANIFEST + _PRESENTATION_MANIFEST,
|
||||
supported_currencies=("RUB",),
|
||||
currency_support_note="YooKassa public payment API examples and limits are RUB-based; treat non-RUB as unsupported unless your YooKassa contract confirms otherwise.",
|
||||
currency_support_url="https://yookassa.ru/developers/payment-acceptance/integration-scenarios/smart-payment",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user