feat: show optional serivce description before payment
This commit is contained in:
@@ -93,6 +93,9 @@ WATA_ENABLED=False #
|
||||
HELEKET_ENABLED=False # Turn on Heleket (crypto payments)
|
||||
# Order of payment methods (top to bottom). Supported: severpay, wata, freekassa, platega, yookassa, stars, cryptopay, heleket
|
||||
PAYMENT_METHODS_ORDER=severpay,wata,yookassa,cryptopay,freekassa,platega,stars,heleket
|
||||
SUBSCRIPTION_PURCHASE_DESCRIPTION_ENABLED=True # Show subscription description before choosing a purchase/renewal period
|
||||
SUBSCRIPTION_PURCHASE_DESCRIPTION_RU=Покупая или продлевая подписку, вы получаете доступ к VPN/прокси-сервису, который помогает защищать ваше соединение и поддерживать стабильный доступ к сети.
|
||||
SUBSCRIPTION_PURCHASE_DESCRIPTION_EN=By buying or renewing a subscription, you get access to a VPN/proxy service that helps protect your connection and keep your access stable.
|
||||
|
||||
# Payment button presentation overrides (all optional; empty = provider defaults)
|
||||
# Text supports per-language overrides: *_LABEL_RU and *_LABEL_EN. Legacy *_LABEL applies to all languages if per-language values are empty.
|
||||
|
||||
@@ -133,6 +133,27 @@ SETTINGS_MANIFEST: List[SettingField] = [
|
||||
"Порядок методов оплаты",
|
||||
"Через запятую, например: severpay,freekassa,yookassa",
|
||||
),
|
||||
SettingField(
|
||||
"SUBSCRIPTION_PURCHASE_DESCRIPTION_ENABLED",
|
||||
"bool",
|
||||
"pricing",
|
||||
"Показывать описание подписки",
|
||||
"Текст появится перед выбором срока покупки или продления.",
|
||||
),
|
||||
SettingField(
|
||||
"SUBSCRIPTION_PURCHASE_DESCRIPTION_RU",
|
||||
"text",
|
||||
"pricing",
|
||||
"Описание подписки (RU)",
|
||||
"Русская версия текста на этапе оплаты.",
|
||||
),
|
||||
SettingField(
|
||||
"SUBSCRIPTION_PURCHASE_DESCRIPTION_EN",
|
||||
"text",
|
||||
"pricing",
|
||||
"Описание подписки (EN)",
|
||||
"Английская версия текста на этапе оплаты.",
|
||||
),
|
||||
# ─── Payment providers (toggles) ───────────────────────────────
|
||||
# Common
|
||||
SettingField("STARS_ENABLED", "bool", "payments", "Telegram Stars", subsection="Общие"),
|
||||
@@ -338,9 +359,7 @@ SETTINGS_MANIFEST: List[SettingField] = [
|
||||
]
|
||||
|
||||
|
||||
def _provider_field_to_setting_field(
|
||||
spec: Any, manifest_field: Any
|
||||
) -> SettingField:
|
||||
def _provider_field_to_setting_field(spec: Any, manifest_field: Any) -> SettingField:
|
||||
return SettingField(
|
||||
key=manifest_field.key,
|
||||
type=manifest_field.type,
|
||||
@@ -445,9 +464,7 @@ def manifest_payload() -> List[dict]:
|
||||
items: List[dict] = []
|
||||
for field in aggregated_manifest():
|
||||
auto_label_i18n_key = f"admin_settings_field_{field.key.lower()}_label"
|
||||
auto_description_i18n_key = (
|
||||
f"admin_settings_field_{field.key.lower()}_description"
|
||||
)
|
||||
auto_description_i18n_key = f"admin_settings_field_{field.key.lower()}_description"
|
||||
|
||||
default_value: Optional[str] = None
|
||||
owner = find_manifest_owner(field.key)
|
||||
|
||||
@@ -679,5 +679,3 @@ async def _create_subscription_payment(
|
||||
)
|
||||
|
||||
return _json_error(400, "payment_unavailable", "Payment method unavailable")
|
||||
|
||||
|
||||
|
||||
@@ -131,6 +131,7 @@ async def _build_user_payload(request: web.Request, user_id: int) -> Dict[str, A
|
||||
"trial_duration_days": int(settings.TRIAL_DURATION_DAYS or 0),
|
||||
"trial_traffic_limit_gb": float(settings.TRIAL_TRAFFIC_LIMIT_GB or 0),
|
||||
"trial_traffic_strategy": getattr(settings, "TRIAL_TRAFFIC_STRATEGY", "NO_RESET"),
|
||||
"subscription_purchase_description": settings.subscription_purchase_description(lang),
|
||||
"email_auth_enabled": settings.email_auth_configured,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -94,6 +94,22 @@ def _tariff_purchase_text(tariff, current_lang: str, i18n: JsonI18n, settings: S
|
||||
return f"{tariff.name(current_lang)}\n{tariff.description(current_lang)}".strip()
|
||||
|
||||
|
||||
def _with_subscription_purchase_description(
|
||||
text: str,
|
||||
settings: Settings,
|
||||
current_lang: str,
|
||||
*,
|
||||
include: bool,
|
||||
) -> str:
|
||||
if not include:
|
||||
return text
|
||||
description_resolver = getattr(settings, "subscription_purchase_description", None)
|
||||
description = description_resolver(current_lang) if callable(description_resolver) else ""
|
||||
if not description:
|
||||
return text
|
||||
return f"{description}\n\n{text}"
|
||||
|
||||
|
||||
async def display_subscription_options(
|
||||
event: Union[types.Message, types.CallbackQuery],
|
||||
i18n_data: dict,
|
||||
@@ -125,6 +141,12 @@ async def display_subscription_options(
|
||||
if len(enabled_tariffs) == 1:
|
||||
tariff = enabled_tariffs[0]
|
||||
text_content = _tariff_purchase_text(tariff, current_lang, i18n, settings)
|
||||
text_content = _with_subscription_purchase_description(
|
||||
text_content,
|
||||
settings,
|
||||
current_lang,
|
||||
include=tariff.billing_model == "period",
|
||||
)
|
||||
reply_markup = _tariff_purchase_markup(
|
||||
tariff,
|
||||
current_lang,
|
||||
@@ -135,6 +157,12 @@ async def display_subscription_options(
|
||||
)
|
||||
else:
|
||||
text_content = get_text("select_subscription_period")
|
||||
text_content = _with_subscription_purchase_description(
|
||||
text_content,
|
||||
settings,
|
||||
current_lang,
|
||||
include=any(tariff.billing_model == "period" for tariff in enabled_tariffs),
|
||||
)
|
||||
reply_markup = get_tariff_catalog_keyboard(
|
||||
enabled_tariffs,
|
||||
current_lang,
|
||||
@@ -174,6 +202,12 @@ async def display_subscription_options(
|
||||
if traffic_mode
|
||||
else get_text("select_subscription_period")
|
||||
)
|
||||
text_content = _with_subscription_purchase_description(
|
||||
text_content,
|
||||
settings,
|
||||
current_lang,
|
||||
include=not traffic_mode,
|
||||
)
|
||||
reply_markup = get_subscription_options_keyboard(
|
||||
options,
|
||||
currency_symbol_val,
|
||||
@@ -248,6 +282,12 @@ async def select_tariff_callback(
|
||||
callback_context=callback_context,
|
||||
)
|
||||
text = _tariff_purchase_text(tariff, current_lang, i18n, settings)
|
||||
text = _with_subscription_purchase_description(
|
||||
text,
|
||||
settings,
|
||||
current_lang,
|
||||
include=tariff.billing_model == "period",
|
||||
)
|
||||
await callback.message.edit_text(text, reply_markup=markup)
|
||||
await callback.answer()
|
||||
|
||||
@@ -284,9 +324,7 @@ async def select_tariff_period_callback(
|
||||
current_lang,
|
||||
i18n,
|
||||
settings,
|
||||
sale_mode=sale_mode_with_callback_context(
|
||||
f"subscription@{tariff.key}", callback_context
|
||||
),
|
||||
sale_mode=sale_mode_with_callback_context(f"subscription@{tariff.key}", callback_context),
|
||||
back_callback=f"tariff:select:{tariff.key}{callback_suffix_for_context(callback_context)}",
|
||||
)
|
||||
await callback.message.edit_text(get_text("choose_payment_method"), reply_markup=markup)
|
||||
|
||||
@@ -293,8 +293,7 @@ def get_subscription_options_keyboard(
|
||||
currency_symbol=currency_symbol_val,
|
||||
)
|
||||
callback_data = (
|
||||
f"subscribe_period:{months}"
|
||||
f"{callback_suffix_for_context(callback_context)}"
|
||||
f"subscribe_period:{months}{callback_suffix_for_context(callback_context)}"
|
||||
)
|
||||
builder.button(text=button_text, callback_data=callback_data)
|
||||
builder.adjust(1)
|
||||
@@ -332,9 +331,7 @@ def get_tariff_catalog_keyboard(
|
||||
)
|
||||
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text=_(key="back_to_main_menu_button"), callback_data=back_callback
|
||||
)
|
||||
InlineKeyboardButton(text=_(key="back_to_main_menu_button"), callback_data=back_callback)
|
||||
)
|
||||
return builder.as_markup()
|
||||
|
||||
@@ -366,9 +363,7 @@ def get_tariff_periods_keyboard(
|
||||
)
|
||||
)
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text=_(key="back_to_main_menu_button"), callback_data=back_callback
|
||||
)
|
||||
InlineKeyboardButton(text=_(key="back_to_main_menu_button"), callback_data=back_callback)
|
||||
)
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ from .registry import (
|
||||
get_spec_presentation,
|
||||
iter_provider_manifest_fields,
|
||||
iter_provider_specs,
|
||||
manifest_field_default,
|
||||
iter_service_keys,
|
||||
iter_unique_provider_routers,
|
||||
manifest_field_default,
|
||||
pending_statuses,
|
||||
provider_emoji_map,
|
||||
provider_label_map,
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Awaitable, Callable, List, Mapping, Optional, Sequence, Type
|
||||
from typing import Any, Awaitable, Callable, Mapping, Optional, Sequence, Type
|
||||
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
@@ -65,7 +65,9 @@ class ProviderManifestField:
|
||||
choices: Optional[Sequence[tuple[str, str]]] = None
|
||||
subsection: Optional[str] = None
|
||||
target: str = "config" # "config" or "presentation" — which bundle slot it writes to
|
||||
attr: Optional[str] = None # attribute name on the target model; defaults to key without env_prefix
|
||||
attr: Optional[str] = (
|
||||
None # attribute name on the target model; defaults to key without env_prefix
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
||||
@@ -349,10 +349,7 @@ async def pay_crypto_callback_handler(
|
||||
await notify_callback_parse_error(callback, translator)
|
||||
return
|
||||
|
||||
if (
|
||||
not cryptopay_service
|
||||
or not getattr(cryptopay_service, "configured", False)
|
||||
):
|
||||
if not cryptopay_service or not getattr(cryptopay_service, "configured", False):
|
||||
await notify_service_unavailable(callback, translator)
|
||||
return
|
||||
|
||||
@@ -390,7 +387,11 @@ 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()
|
||||
config = (
|
||||
bundle.config
|
||||
if bundle and isinstance(bundle.config, CryptoPayConfig)
|
||||
else CryptoPayConfig()
|
||||
)
|
||||
return CryptoPayService(
|
||||
bot=ctx.bot,
|
||||
settings=ctx.settings,
|
||||
@@ -422,40 +423,99 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
|
||||
|
||||
_PRESENTATION_MANIFEST = tuple(
|
||||
ProviderManifestField(
|
||||
key=key, type=type_, label=label, description=description,
|
||||
placeholder=placeholder, subsection="CryptoPay",
|
||||
target="presentation", attr=attr,
|
||||
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"),
|
||||
(
|
||||
"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"),
|
||||
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",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -71,9 +71,7 @@ class FreeKassaConfig(ProviderEnvConfig):
|
||||
API_KEY: Optional[str] = None
|
||||
PAYMENT_IP: Optional[str] = None
|
||||
PAYMENT_METHOD_ID: Optional[int] = None
|
||||
TRUSTED_IPS: str = Field(
|
||||
default="168.119.157.136,168.119.60.227,178.154.197.79,51.250.54.238"
|
||||
)
|
||||
TRUSTED_IPS: str = Field(default="168.119.157.136,168.119.60.227,178.154.197.79,51.250.54.238")
|
||||
|
||||
@field_validator("PAYMENT_METHOD_ID", mode="before")
|
||||
@classmethod
|
||||
@@ -85,7 +83,11 @@ class FreeKassaConfig(ProviderEnvConfig):
|
||||
return v
|
||||
|
||||
@field_validator(
|
||||
"MERCHANT_ID", "FIRST_SECRET", "SECOND_SECRET", "API_KEY", "PAYMENT_IP",
|
||||
"MERCHANT_ID",
|
||||
"FIRST_SECRET",
|
||||
"SECOND_SECRET",
|
||||
"API_KEY",
|
||||
"PAYMENT_IP",
|
||||
mode="before",
|
||||
)
|
||||
@classmethod
|
||||
@@ -505,8 +507,10 @@ async def pay_fk_callback_handler(
|
||||
provider_identifier = first_value(response_data, "orderHash", "orderId")
|
||||
lead_text: Optional[str] = None
|
||||
if success and location:
|
||||
order_id_display = first_value(response_data, "orderId") or provider_identifier or str(
|
||||
payment_record.payment_id
|
||||
order_id_display = (
|
||||
first_value(response_data, "orderId")
|
||||
or provider_identifier
|
||||
or str(payment_record.payment_id)
|
||||
)
|
||||
lead_text = translator(
|
||||
"free_kassa_order_info",
|
||||
@@ -532,7 +536,11 @@ async def pay_fk_callback_handler(
|
||||
|
||||
def create_service(ctx: ServiceFactoryContext) -> FreeKassaService:
|
||||
bundle = ctx.config_for("freekassa_service")
|
||||
config = bundle.config if bundle and isinstance(bundle.config, FreeKassaConfig) else FreeKassaConfig()
|
||||
config = (
|
||||
bundle.config
|
||||
if bundle and isinstance(bundle.config, FreeKassaConfig)
|
||||
else FreeKassaConfig()
|
||||
)
|
||||
return FreeKassaService(
|
||||
bot=ctx.bot,
|
||||
settings=ctx.settings,
|
||||
@@ -584,51 +592,130 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
|
||||
|
||||
_PRESENTATION_MANIFEST = tuple(
|
||||
ProviderManifestField(
|
||||
key=key, type=type_, label=label, description=description,
|
||||
placeholder=placeholder, subsection="FreeKassa",
|
||||
target="presentation", attr=attr,
|
||||
key=key,
|
||||
type=type_,
|
||||
label=label,
|
||||
description=description,
|
||||
placeholder=placeholder,
|
||||
subsection="FreeKassa",
|
||||
target="presentation",
|
||||
attr=attr,
|
||||
)
|
||||
for key, type_, label, description, placeholder, attr in (
|
||||
("PAYMENT_FREEKASSA_WEBAPP_LABEL_RU", "string", "WebApp button text (RU)",
|
||||
"Custom Russian text shown in the Web App payment method button.", "", "WEBAPP_LABEL_RU"),
|
||||
("PAYMENT_FREEKASSA_WEBAPP_LABEL_EN", "string", "WebApp button text (EN)",
|
||||
"Custom English text shown in the Web App payment method button.", "", "WEBAPP_LABEL_EN"),
|
||||
("PAYMENT_FREEKASSA_WEBAPP_ICON", "icon", "WebApp button icon",
|
||||
"Lucide icon name rendered inside the Web App payment method button.",
|
||||
"Smartphone", "WEBAPP_ICON"),
|
||||
("PAYMENT_FREEKASSA_TELEGRAM_LABEL_RU", "string", "Telegram button text (RU)",
|
||||
"Custom Russian text shown in Telegram bot payment buttons.", "", "TELEGRAM_LABEL_RU"),
|
||||
("PAYMENT_FREEKASSA_TELEGRAM_LABEL_EN", "string", "Telegram button text (EN)",
|
||||
"Custom English text shown in Telegram bot payment buttons.", "", "TELEGRAM_LABEL_EN"),
|
||||
("PAYMENT_FREEKASSA_TELEGRAM_EMOJI", "string", "Telegram button emoji",
|
||||
"Emoji prepended to the Telegram bot payment button when customized.",
|
||||
"📱", "TELEGRAM_EMOJI"),
|
||||
(
|
||||
"PAYMENT_FREEKASSA_WEBAPP_LABEL_RU",
|
||||
"string",
|
||||
"WebApp button text (RU)",
|
||||
"Custom Russian text shown in the Web App payment method button.",
|
||||
"",
|
||||
"WEBAPP_LABEL_RU",
|
||||
),
|
||||
(
|
||||
"PAYMENT_FREEKASSA_WEBAPP_LABEL_EN",
|
||||
"string",
|
||||
"WebApp button text (EN)",
|
||||
"Custom English text shown in the Web App payment method button.",
|
||||
"",
|
||||
"WEBAPP_LABEL_EN",
|
||||
),
|
||||
(
|
||||
"PAYMENT_FREEKASSA_WEBAPP_ICON",
|
||||
"icon",
|
||||
"WebApp button icon",
|
||||
"Lucide icon name rendered inside the Web App payment method button.",
|
||||
"Smartphone",
|
||||
"WEBAPP_ICON",
|
||||
),
|
||||
(
|
||||
"PAYMENT_FREEKASSA_TELEGRAM_LABEL_RU",
|
||||
"string",
|
||||
"Telegram button text (RU)",
|
||||
"Custom Russian text shown in Telegram bot payment buttons.",
|
||||
"",
|
||||
"TELEGRAM_LABEL_RU",
|
||||
),
|
||||
(
|
||||
"PAYMENT_FREEKASSA_TELEGRAM_LABEL_EN",
|
||||
"string",
|
||||
"Telegram button text (EN)",
|
||||
"Custom English text shown in Telegram bot payment buttons.",
|
||||
"",
|
||||
"TELEGRAM_LABEL_EN",
|
||||
),
|
||||
(
|
||||
"PAYMENT_FREEKASSA_TELEGRAM_EMOJI",
|
||||
"string",
|
||||
"Telegram button emoji",
|
||||
"Emoji prepended to the Telegram bot payment button when customized.",
|
||||
"📱",
|
||||
"TELEGRAM_EMOJI",
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
_CONFIG_MANIFEST = (
|
||||
ProviderManifestField("FREEKASSA_ENABLED", "bool", "Включена",
|
||||
subsection="FreeKassa", attr="ENABLED"),
|
||||
ProviderManifestField("FREEKASSA_MERCHANT_ID", "string", "Merchant ID",
|
||||
subsection="FreeKassa", attr="MERCHANT_ID"),
|
||||
ProviderManifestField("FREEKASSA_FIRST_SECRET", "string", "First secret",
|
||||
subsection="FreeKassa", secret=True, attr="FIRST_SECRET"),
|
||||
ProviderManifestField("FREEKASSA_SECOND_SECRET", "string", "Second secret",
|
||||
subsection="FreeKassa", secret=True, attr="SECOND_SECRET"),
|
||||
ProviderManifestField("FREEKASSA_API_KEY", "string", "API key",
|
||||
subsection="FreeKassa", secret=True, attr="API_KEY"),
|
||||
ProviderManifestField("FREEKASSA_PAYMENT_URL", "url", "Payment URL",
|
||||
placeholder="https://pay.freekassa.ru/",
|
||||
subsection="FreeKassa", attr="PAYMENT_URL"),
|
||||
ProviderManifestField("FREEKASSA_PAYMENT_METHOD_ID", "int", "Payment method ID",
|
||||
description="See https://merchant.freekassa.net/settings/currencies",
|
||||
subsection="FreeKassa", attr="PAYMENT_METHOD_ID"),
|
||||
ProviderManifestField("FREEKASSA_PAYMENT_IP", "string", "Server IP",
|
||||
description="Public IP address reported to FreeKassa.",
|
||||
subsection="FreeKassa", attr="PAYMENT_IP"),
|
||||
ProviderManifestField("FREEKASSA_TRUSTED_IPS", "string", "Trusted IPs",
|
||||
description="Comma-separated IP addresses accepted for FreeKassa webhooks.",
|
||||
subsection="FreeKassa", attr="TRUSTED_IPS"),
|
||||
ProviderManifestField(
|
||||
"FREEKASSA_ENABLED", "bool", "Включена", subsection="FreeKassa", attr="ENABLED"
|
||||
),
|
||||
ProviderManifestField(
|
||||
"FREEKASSA_MERCHANT_ID", "string", "Merchant ID", subsection="FreeKassa", attr="MERCHANT_ID"
|
||||
),
|
||||
ProviderManifestField(
|
||||
"FREEKASSA_FIRST_SECRET",
|
||||
"string",
|
||||
"First secret",
|
||||
subsection="FreeKassa",
|
||||
secret=True,
|
||||
attr="FIRST_SECRET",
|
||||
),
|
||||
ProviderManifestField(
|
||||
"FREEKASSA_SECOND_SECRET",
|
||||
"string",
|
||||
"Second secret",
|
||||
subsection="FreeKassa",
|
||||
secret=True,
|
||||
attr="SECOND_SECRET",
|
||||
),
|
||||
ProviderManifestField(
|
||||
"FREEKASSA_API_KEY",
|
||||
"string",
|
||||
"API key",
|
||||
subsection="FreeKassa",
|
||||
secret=True,
|
||||
attr="API_KEY",
|
||||
),
|
||||
ProviderManifestField(
|
||||
"FREEKASSA_PAYMENT_URL",
|
||||
"url",
|
||||
"Payment URL",
|
||||
placeholder="https://pay.freekassa.ru/",
|
||||
subsection="FreeKassa",
|
||||
attr="PAYMENT_URL",
|
||||
),
|
||||
ProviderManifestField(
|
||||
"FREEKASSA_PAYMENT_METHOD_ID",
|
||||
"int",
|
||||
"Payment method ID",
|
||||
description="See https://merchant.freekassa.net/settings/currencies",
|
||||
subsection="FreeKassa",
|
||||
attr="PAYMENT_METHOD_ID",
|
||||
),
|
||||
ProviderManifestField(
|
||||
"FREEKASSA_PAYMENT_IP",
|
||||
"string",
|
||||
"Server IP",
|
||||
description="Public IP address reported to FreeKassa.",
|
||||
subsection="FreeKassa",
|
||||
attr="PAYMENT_IP",
|
||||
),
|
||||
ProviderManifestField(
|
||||
"FREEKASSA_TRUSTED_IPS",
|
||||
"string",
|
||||
"Trusted IPs",
|
||||
description="Comma-separated IP addresses accepted for FreeKassa webhooks.",
|
||||
subsection="FreeKassa",
|
||||
attr="TRUSTED_IPS",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -93,7 +93,12 @@ class HeleketConfig(ProviderEnvConfig):
|
||||
return min(43200, max(300, value))
|
||||
|
||||
@field_validator(
|
||||
"MERCHANT_ID", "API_KEY", "TO_CURRENCY", "NETWORK", "RETURN_URL", "SUCCESS_URL",
|
||||
"MERCHANT_ID",
|
||||
"API_KEY",
|
||||
"TO_CURRENCY",
|
||||
"NETWORK",
|
||||
"RETURN_URL",
|
||||
"SUCCESS_URL",
|
||||
mode="before",
|
||||
)
|
||||
@classmethod
|
||||
@@ -644,7 +649,9 @@ async def heleket_webhook_route(request: web.Request) -> web.Response:
|
||||
|
||||
def create_service(ctx: ServiceFactoryContext) -> HeleketService:
|
||||
bundle = ctx.config_for("heleket_service")
|
||||
config = bundle.config if bundle and isinstance(bundle.config, HeleketConfig) else HeleketConfig()
|
||||
config = (
|
||||
bundle.config if bundle and isinstance(bundle.config, HeleketConfig) else HeleketConfig()
|
||||
)
|
||||
return HeleketService(
|
||||
bot=ctx.bot,
|
||||
settings=ctx.settings,
|
||||
@@ -669,51 +676,142 @@ _PRESENTATION_MANIFEST = tuple(
|
||||
attr=attr,
|
||||
)
|
||||
for key, type_, label, description, placeholder, attr in (
|
||||
("PAYMENT_HELEKET_WEBAPP_LABEL_RU", "string", "WebApp button text (RU)",
|
||||
"Custom Russian text shown in the Web App payment method button.", "", "WEBAPP_LABEL_RU"),
|
||||
("PAYMENT_HELEKET_WEBAPP_LABEL_EN", "string", "WebApp button text (EN)",
|
||||
"Custom English text shown in the Web App payment method button.", "", "WEBAPP_LABEL_EN"),
|
||||
("PAYMENT_HELEKET_WEBAPP_ICON", "icon", "WebApp button icon",
|
||||
"Lucide icon name rendered inside the Web App payment method button.", "Bitcoin", "WEBAPP_ICON"),
|
||||
("PAYMENT_HELEKET_TELEGRAM_LABEL_RU", "string", "Telegram button text (RU)",
|
||||
"Custom Russian text shown in Telegram bot payment buttons.", "", "TELEGRAM_LABEL_RU"),
|
||||
("PAYMENT_HELEKET_TELEGRAM_LABEL_EN", "string", "Telegram button text (EN)",
|
||||
"Custom English text shown in Telegram bot payment buttons.", "", "TELEGRAM_LABEL_EN"),
|
||||
("PAYMENT_HELEKET_TELEGRAM_EMOJI", "string", "Telegram button emoji",
|
||||
"Emoji prepended to the Telegram bot payment button when customized.", "🪙", "TELEGRAM_EMOJI"),
|
||||
(
|
||||
"PAYMENT_HELEKET_WEBAPP_LABEL_RU",
|
||||
"string",
|
||||
"WebApp button text (RU)",
|
||||
"Custom Russian text shown in the Web App payment method button.",
|
||||
"",
|
||||
"WEBAPP_LABEL_RU",
|
||||
),
|
||||
(
|
||||
"PAYMENT_HELEKET_WEBAPP_LABEL_EN",
|
||||
"string",
|
||||
"WebApp button text (EN)",
|
||||
"Custom English text shown in the Web App payment method button.",
|
||||
"",
|
||||
"WEBAPP_LABEL_EN",
|
||||
),
|
||||
(
|
||||
"PAYMENT_HELEKET_WEBAPP_ICON",
|
||||
"icon",
|
||||
"WebApp button icon",
|
||||
"Lucide icon name rendered inside the Web App payment method button.",
|
||||
"Bitcoin",
|
||||
"WEBAPP_ICON",
|
||||
),
|
||||
(
|
||||
"PAYMENT_HELEKET_TELEGRAM_LABEL_RU",
|
||||
"string",
|
||||
"Telegram button text (RU)",
|
||||
"Custom Russian text shown in Telegram bot payment buttons.",
|
||||
"",
|
||||
"TELEGRAM_LABEL_RU",
|
||||
),
|
||||
(
|
||||
"PAYMENT_HELEKET_TELEGRAM_LABEL_EN",
|
||||
"string",
|
||||
"Telegram button text (EN)",
|
||||
"Custom English text shown in Telegram bot payment buttons.",
|
||||
"",
|
||||
"TELEGRAM_LABEL_EN",
|
||||
),
|
||||
(
|
||||
"PAYMENT_HELEKET_TELEGRAM_EMOJI",
|
||||
"string",
|
||||
"Telegram button emoji",
|
||||
"Emoji prepended to the Telegram bot payment button when customized.",
|
||||
"🪙",
|
||||
"TELEGRAM_EMOJI",
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
_CONFIG_MANIFEST = (
|
||||
ProviderManifestField("HELEKET_ENABLED", "bool", "Enabled", subsection="Heleket", attr="ENABLED"),
|
||||
ProviderManifestField("HELEKET_MERCHANT_ID", "string", "Merchant ID", subsection="Heleket",
|
||||
secret=True, attr="MERCHANT_ID"),
|
||||
ProviderManifestField("HELEKET_API_KEY", "string", "Payment API key", subsection="Heleket",
|
||||
secret=True, attr="API_KEY"),
|
||||
ProviderManifestField("HELEKET_BASE_URL", "url", "Base URL",
|
||||
placeholder="https://api.heleket.com", subsection="Heleket", attr="BASE_URL"),
|
||||
ProviderManifestField("HELEKET_CURRENCY", "string", "Invoice currency",
|
||||
description="Fiat or crypto code (RUB, USD, USDT).",
|
||||
placeholder="RUB", subsection="Heleket", attr="CURRENCY"),
|
||||
ProviderManifestField("HELEKET_TO_CURRENCY", "string", "Target crypto",
|
||||
description="Optional target cryptocurrency for conversion.",
|
||||
subsection="Heleket", attr="TO_CURRENCY"),
|
||||
ProviderManifestField("HELEKET_NETWORK", "string", "Blockchain network",
|
||||
description="Optional blockchain network code (tron, bsc, eth).",
|
||||
subsection="Heleket", attr="NETWORK"),
|
||||
ProviderManifestField("HELEKET_RETURN_URL", "url", "Return URL", subsection="Heleket",
|
||||
attr="RETURN_URL"),
|
||||
ProviderManifestField("HELEKET_SUCCESS_URL", "url", "Success URL", subsection="Heleket",
|
||||
attr="SUCCESS_URL"),
|
||||
ProviderManifestField("HELEKET_LIFETIME_SECONDS", "int", "Invoice lifetime (seconds)",
|
||||
description="300..43200; Heleket defaults to 3600.",
|
||||
subsection="Heleket", min=300, max=43200, attr="LIFETIME_SECONDS"),
|
||||
ProviderManifestField("HELEKET_VERIFY_WEBHOOK_SIGNATURE", "bool", "Verify webhook signature",
|
||||
subsection="Heleket", attr="VERIFY_WEBHOOK_SIGNATURE"),
|
||||
ProviderManifestField("HELEKET_TRUSTED_IPS", "string", "Trusted IPs",
|
||||
description="Comma-separated IP addresses accepted for Heleket webhooks.",
|
||||
subsection="Heleket", attr="TRUSTED_IPS"),
|
||||
ProviderManifestField(
|
||||
"HELEKET_ENABLED", "bool", "Enabled", subsection="Heleket", attr="ENABLED"
|
||||
),
|
||||
ProviderManifestField(
|
||||
"HELEKET_MERCHANT_ID",
|
||||
"string",
|
||||
"Merchant ID",
|
||||
subsection="Heleket",
|
||||
secret=True,
|
||||
attr="MERCHANT_ID",
|
||||
),
|
||||
ProviderManifestField(
|
||||
"HELEKET_API_KEY",
|
||||
"string",
|
||||
"Payment API key",
|
||||
subsection="Heleket",
|
||||
secret=True,
|
||||
attr="API_KEY",
|
||||
),
|
||||
ProviderManifestField(
|
||||
"HELEKET_BASE_URL",
|
||||
"url",
|
||||
"Base URL",
|
||||
placeholder="https://api.heleket.com",
|
||||
subsection="Heleket",
|
||||
attr="BASE_URL",
|
||||
),
|
||||
ProviderManifestField(
|
||||
"HELEKET_CURRENCY",
|
||||
"string",
|
||||
"Invoice currency",
|
||||
description="Fiat or crypto code (RUB, USD, USDT).",
|
||||
placeholder="RUB",
|
||||
subsection="Heleket",
|
||||
attr="CURRENCY",
|
||||
),
|
||||
ProviderManifestField(
|
||||
"HELEKET_TO_CURRENCY",
|
||||
"string",
|
||||
"Target crypto",
|
||||
description="Optional target cryptocurrency for conversion.",
|
||||
subsection="Heleket",
|
||||
attr="TO_CURRENCY",
|
||||
),
|
||||
ProviderManifestField(
|
||||
"HELEKET_NETWORK",
|
||||
"string",
|
||||
"Blockchain network",
|
||||
description="Optional blockchain network code (tron, bsc, eth).",
|
||||
subsection="Heleket",
|
||||
attr="NETWORK",
|
||||
),
|
||||
ProviderManifestField(
|
||||
"HELEKET_RETURN_URL", "url", "Return URL", subsection="Heleket", attr="RETURN_URL"
|
||||
),
|
||||
ProviderManifestField(
|
||||
"HELEKET_SUCCESS_URL", "url", "Success URL", subsection="Heleket", attr="SUCCESS_URL"
|
||||
),
|
||||
ProviderManifestField(
|
||||
"HELEKET_LIFETIME_SECONDS",
|
||||
"int",
|
||||
"Invoice lifetime (seconds)",
|
||||
description="300..43200; Heleket defaults to 3600.",
|
||||
subsection="Heleket",
|
||||
min=300,
|
||||
max=43200,
|
||||
attr="LIFETIME_SECONDS",
|
||||
),
|
||||
ProviderManifestField(
|
||||
"HELEKET_VERIFY_WEBHOOK_SIGNATURE",
|
||||
"bool",
|
||||
"Verify webhook signature",
|
||||
subsection="Heleket",
|
||||
attr="VERIFY_WEBHOOK_SIGNATURE",
|
||||
),
|
||||
ProviderManifestField(
|
||||
"HELEKET_TRUSTED_IPS",
|
||||
"string",
|
||||
"Trusted IPs",
|
||||
description="Comma-separated IP addresses accepted for Heleket webhooks.",
|
||||
subsection="Heleket",
|
||||
attr="TRUSTED_IPS",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -429,7 +429,11 @@ async def pay_platega_callback_handler(
|
||||
return
|
||||
|
||||
callback_prefix, _, _ = (callback.data or "").partition(":")
|
||||
variant = _resolve_platega_variant(callback_prefix, platega_service.config) if platega_service else None
|
||||
variant = (
|
||||
_resolve_platega_variant(callback_prefix, platega_service.config)
|
||||
if platega_service
|
||||
else None
|
||||
)
|
||||
if variant is None:
|
||||
await safe_callback_answer(callback)
|
||||
return
|
||||
@@ -513,7 +517,9 @@ async def pay_platega_callback_handler(
|
||||
|
||||
def create_service(ctx: ServiceFactoryContext) -> PlategaService:
|
||||
bundle = ctx.config_for("platega_service")
|
||||
config = bundle.config if bundle and isinstance(bundle.config, PlategaConfig) else PlategaConfig()
|
||||
config = (
|
||||
bundle.config if bundle and isinstance(bundle.config, PlategaConfig) else PlategaConfig()
|
||||
)
|
||||
return PlategaService(
|
||||
bot=ctx.bot,
|
||||
settings=ctx.settings,
|
||||
@@ -613,52 +619,109 @@ def _platega_presentation_manifest(subsection: str, default_icon: str, prefix: s
|
||||
attr=attr,
|
||||
)
|
||||
for suffix_key, type_, label, description, placeholder, attr in (
|
||||
("WEBAPP_LABEL_RU", "string", "WebApp button text (RU)",
|
||||
"Custom Russian text shown in the Web App payment method button.",
|
||||
"", "WEBAPP_LABEL_RU"),
|
||||
("WEBAPP_LABEL_EN", "string", "WebApp button text (EN)",
|
||||
"Custom English text shown in the Web App payment method button.",
|
||||
"", "WEBAPP_LABEL_EN"),
|
||||
("WEBAPP_ICON", "icon", "WebApp button icon",
|
||||
"Lucide icon name rendered inside the Web App payment method button.",
|
||||
default_icon, "WEBAPP_ICON"),
|
||||
("TELEGRAM_LABEL_RU", "string", "Telegram button text (RU)",
|
||||
"Custom Russian text shown in Telegram bot payment buttons.",
|
||||
"", "TELEGRAM_LABEL_RU"),
|
||||
("TELEGRAM_LABEL_EN", "string", "Telegram button text (EN)",
|
||||
"Custom English text shown in Telegram bot payment buttons.",
|
||||
"", "TELEGRAM_LABEL_EN"),
|
||||
("TELEGRAM_EMOJI", "string", "Telegram button emoji",
|
||||
"Emoji prepended to the Telegram bot payment button when customized.",
|
||||
"", "TELEGRAM_EMOJI"),
|
||||
(
|
||||
"WEBAPP_LABEL_RU",
|
||||
"string",
|
||||
"WebApp button text (RU)",
|
||||
"Custom Russian text shown in the Web App payment method button.",
|
||||
"",
|
||||
"WEBAPP_LABEL_RU",
|
||||
),
|
||||
(
|
||||
"WEBAPP_LABEL_EN",
|
||||
"string",
|
||||
"WebApp button text (EN)",
|
||||
"Custom English text shown in the Web App payment method button.",
|
||||
"",
|
||||
"WEBAPP_LABEL_EN",
|
||||
),
|
||||
(
|
||||
"WEBAPP_ICON",
|
||||
"icon",
|
||||
"WebApp button icon",
|
||||
"Lucide icon name rendered inside the Web App payment method button.",
|
||||
default_icon,
|
||||
"WEBAPP_ICON",
|
||||
),
|
||||
(
|
||||
"TELEGRAM_LABEL_RU",
|
||||
"string",
|
||||
"Telegram button text (RU)",
|
||||
"Custom Russian text shown in Telegram bot payment buttons.",
|
||||
"",
|
||||
"TELEGRAM_LABEL_RU",
|
||||
),
|
||||
(
|
||||
"TELEGRAM_LABEL_EN",
|
||||
"string",
|
||||
"Telegram button text (EN)",
|
||||
"Custom English text shown in Telegram bot payment buttons.",
|
||||
"",
|
||||
"TELEGRAM_LABEL_EN",
|
||||
),
|
||||
(
|
||||
"TELEGRAM_EMOJI",
|
||||
"string",
|
||||
"Telegram button emoji",
|
||||
"Emoji prepended to the Telegram bot payment button when customized.",
|
||||
"",
|
||||
"TELEGRAM_EMOJI",
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
_CONFIG_MANIFEST = (
|
||||
ProviderManifestField("PLATEGA_ENABLED", "bool", "Включена",
|
||||
subsection="Platega", attr="ENABLED"),
|
||||
ProviderManifestField("PLATEGA_BASE_URL", "url", "Base URL",
|
||||
placeholder="https://app.platega.io",
|
||||
subsection="Platega", attr="BASE_URL"),
|
||||
ProviderManifestField("PLATEGA_MERCHANT_ID", "string", "Merchant ID",
|
||||
subsection="Platega", attr="MERCHANT_ID"),
|
||||
ProviderManifestField("PLATEGA_SECRET", "string", "Secret",
|
||||
subsection="Platega", secret=True, attr="SECRET"),
|
||||
ProviderManifestField("PLATEGA_PAYMENT_METHOD", "int", "Метод оплаты (legacy)",
|
||||
subsection="Platega", attr="PAYMENT_METHOD"),
|
||||
ProviderManifestField("PLATEGA_SBP_ENABLED", "bool", "SBP-кнопка",
|
||||
subsection="Platega", attr="SBP_ENABLED"),
|
||||
ProviderManifestField("PLATEGA_SBP_METHOD", "int", "SBP method ID",
|
||||
subsection="Platega", attr="SBP_METHOD"),
|
||||
ProviderManifestField("PLATEGA_CRYPTO_ENABLED", "bool", "Crypto-кнопка",
|
||||
subsection="Platega", attr="CRYPTO_ENABLED"),
|
||||
ProviderManifestField("PLATEGA_CRYPTO_METHOD", "int", "Crypto method ID",
|
||||
subsection="Platega", attr="CRYPTO_METHOD"),
|
||||
ProviderManifestField("PLATEGA_RETURN_URL", "url", "Return URL",
|
||||
subsection="Platega", attr="RETURN_URL"),
|
||||
ProviderManifestField("PLATEGA_FAILED_URL", "url", "Failed URL",
|
||||
subsection="Platega", attr="FAILED_URL"),
|
||||
ProviderManifestField(
|
||||
"PLATEGA_ENABLED", "bool", "Включена", subsection="Platega", attr="ENABLED"
|
||||
),
|
||||
ProviderManifestField(
|
||||
"PLATEGA_BASE_URL",
|
||||
"url",
|
||||
"Base URL",
|
||||
placeholder="https://app.platega.io",
|
||||
subsection="Platega",
|
||||
attr="BASE_URL",
|
||||
),
|
||||
ProviderManifestField(
|
||||
"PLATEGA_MERCHANT_ID", "string", "Merchant ID", subsection="Platega", attr="MERCHANT_ID"
|
||||
),
|
||||
ProviderManifestField(
|
||||
"PLATEGA_SECRET", "string", "Secret", subsection="Platega", secret=True, attr="SECRET"
|
||||
),
|
||||
ProviderManifestField(
|
||||
"PLATEGA_PAYMENT_METHOD",
|
||||
"int",
|
||||
"Метод оплаты (legacy)",
|
||||
subsection="Platega",
|
||||
attr="PAYMENT_METHOD",
|
||||
),
|
||||
ProviderManifestField(
|
||||
"PLATEGA_SBP_ENABLED", "bool", "SBP-кнопка", subsection="Platega", attr="SBP_ENABLED"
|
||||
),
|
||||
ProviderManifestField(
|
||||
"PLATEGA_SBP_METHOD", "int", "SBP method ID", subsection="Platega", attr="SBP_METHOD"
|
||||
),
|
||||
ProviderManifestField(
|
||||
"PLATEGA_CRYPTO_ENABLED",
|
||||
"bool",
|
||||
"Crypto-кнопка",
|
||||
subsection="Platega",
|
||||
attr="CRYPTO_ENABLED",
|
||||
),
|
||||
ProviderManifestField(
|
||||
"PLATEGA_CRYPTO_METHOD",
|
||||
"int",
|
||||
"Crypto method ID",
|
||||
subsection="Platega",
|
||||
attr="CRYPTO_METHOD",
|
||||
),
|
||||
ProviderManifestField(
|
||||
"PLATEGA_RETURN_URL", "url", "Return URL", subsection="Platega", attr="RETURN_URL"
|
||||
),
|
||||
ProviderManifestField(
|
||||
"PLATEGA_FAILED_URL", "url", "Failed URL", subsection="Platega", attr="FAILED_URL"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -672,7 +735,9 @@ SBP_SPEC = PaymentProviderSpec(
|
||||
telegram_labels={"ru": "Оплата через СБП", "en": "Pay via SBP"},
|
||||
telegram_emoji="🏦",
|
||||
pending_status="pending_platega",
|
||||
enabled=lambda config: bool(getattr(config, "ENABLED", False) and getattr(config, "SBP_ENABLED", False)),
|
||||
enabled=lambda config: bool(
|
||||
getattr(config, "ENABLED", False) and getattr(config, "SBP_ENABLED", False)
|
||||
),
|
||||
service_key="platega_service",
|
||||
callback_prefix="pay_platega_sbp",
|
||||
aliases=("platega",),
|
||||
@@ -683,9 +748,8 @@ SBP_SPEC = PaymentProviderSpec(
|
||||
create_webapp_payment=create_sbp_webapp_payment,
|
||||
config_class=PlategaConfig,
|
||||
presentation_class=PlategaSbpPresentation,
|
||||
manifest_fields=_CONFIG_MANIFEST + _platega_presentation_manifest(
|
||||
"Platega SBP", "CreditCard", "PLATEGA_SBP"
|
||||
),
|
||||
manifest_fields=_CONFIG_MANIFEST
|
||||
+ _platega_presentation_manifest("Platega SBP", "CreditCard", "PLATEGA_SBP"),
|
||||
)
|
||||
|
||||
CRYPTO_SPEC = PaymentProviderSpec(
|
||||
@@ -700,15 +764,15 @@ CRYPTO_SPEC = PaymentProviderSpec(
|
||||
pending_status="pending_platega",
|
||||
# Uses the same PlategaConfig as SBP_SPEC (shared service_key); enable
|
||||
# flag combines the global PLATEGA_ENABLED with the per-button toggle.
|
||||
enabled=lambda config: bool(getattr(config, "ENABLED", False) and getattr(config, "CRYPTO_ENABLED", False)),
|
||||
enabled=lambda config: bool(
|
||||
getattr(config, "ENABLED", False) and getattr(config, "CRYPTO_ENABLED", False)
|
||||
),
|
||||
service_key="platega_service",
|
||||
callback_prefix="pay_platega_crypto",
|
||||
create_webapp_payment=create_crypto_webapp_payment,
|
||||
config_class=PlategaConfig,
|
||||
presentation_class=PlategaCryptoPresentation,
|
||||
manifest_fields=_platega_presentation_manifest(
|
||||
"Platega Crypto", "Bitcoin", "PLATEGA_CRYPTO"
|
||||
),
|
||||
manifest_fields=_platega_presentation_manifest("Platega Crypto", "Bitcoin", "PLATEGA_CRYPTO"),
|
||||
)
|
||||
|
||||
SPECS = (SBP_SPEC, CRYPTO_SPEC)
|
||||
|
||||
@@ -211,10 +211,7 @@ def resolve_provider_presentation(
|
||||
or _localized_default(spec.webapp_labels, lang, spec.webapp_label)
|
||||
or spec.label
|
||||
)
|
||||
webapp_icon = (
|
||||
_bare_setting_value(settings, spec, "WEBAPP_ICON")
|
||||
or spec.webapp_icon
|
||||
)
|
||||
webapp_icon = _bare_setting_value(settings, spec, "WEBAPP_ICON") or spec.webapp_icon
|
||||
telegram_label_override = _localized_setting_value(
|
||||
settings,
|
||||
spec,
|
||||
@@ -351,15 +348,9 @@ def manifest_field_default(
|
||||
return None
|
||||
attr = manifest_field.attr or manifest_field.key
|
||||
if attr == "WEBAPP_LABEL_RU":
|
||||
return (
|
||||
_localized_default(spec.webapp_labels, "ru", spec.webapp_label)
|
||||
or spec.label
|
||||
)
|
||||
return _localized_default(spec.webapp_labels, "ru", spec.webapp_label) or spec.label
|
||||
if attr == "WEBAPP_LABEL_EN":
|
||||
return (
|
||||
_localized_default(spec.webapp_labels, "en", spec.webapp_label)
|
||||
or spec.label
|
||||
)
|
||||
return _localized_default(spec.webapp_labels, "en", spec.webapp_label) or spec.label
|
||||
if attr == "WEBAPP_ICON":
|
||||
return spec.webapp_icon
|
||||
if attr == "TELEGRAM_LABEL_RU":
|
||||
|
||||
@@ -453,7 +453,9 @@ async def pay_severpay_callback_handler(
|
||||
|
||||
def create_service(ctx: ServiceFactoryContext) -> SeverPayService:
|
||||
bundle = ctx.config_for("severpay_service")
|
||||
config = bundle.config if bundle and isinstance(bundle.config, SeverPayConfig) else SeverPayConfig()
|
||||
config = (
|
||||
bundle.config if bundle and isinstance(bundle.config, SeverPayConfig) else SeverPayConfig()
|
||||
)
|
||||
return SeverPayService(
|
||||
bot=ctx.bot,
|
||||
settings=ctx.settings,
|
||||
@@ -506,42 +508,96 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
|
||||
|
||||
_PRESENTATION_MANIFEST = tuple(
|
||||
ProviderManifestField(
|
||||
key=key, type=type_, label=label, description=description,
|
||||
placeholder=placeholder, subsection="SeverPay",
|
||||
target="presentation", attr=attr,
|
||||
key=key,
|
||||
type=type_,
|
||||
label=label,
|
||||
description=description,
|
||||
placeholder=placeholder,
|
||||
subsection="SeverPay",
|
||||
target="presentation",
|
||||
attr=attr,
|
||||
)
|
||||
for key, type_, label, description, placeholder, attr in (
|
||||
("PAYMENT_SEVERPAY_WEBAPP_LABEL_RU", "string", "WebApp button text (RU)",
|
||||
"Custom Russian text shown in the Web App payment method button.", "", "WEBAPP_LABEL_RU"),
|
||||
("PAYMENT_SEVERPAY_WEBAPP_LABEL_EN", "string", "WebApp button text (EN)",
|
||||
"Custom English text shown in the Web App payment method button.", "", "WEBAPP_LABEL_EN"),
|
||||
("PAYMENT_SEVERPAY_WEBAPP_ICON", "icon", "WebApp button icon",
|
||||
"Lucide icon name rendered inside the Web App payment method button.",
|
||||
"CreditCard", "WEBAPP_ICON"),
|
||||
("PAYMENT_SEVERPAY_TELEGRAM_LABEL_RU", "string", "Telegram button text (RU)",
|
||||
"Custom Russian text shown in Telegram bot payment buttons.", "", "TELEGRAM_LABEL_RU"),
|
||||
("PAYMENT_SEVERPAY_TELEGRAM_LABEL_EN", "string", "Telegram button text (EN)",
|
||||
"Custom English text shown in Telegram bot payment buttons.", "", "TELEGRAM_LABEL_EN"),
|
||||
("PAYMENT_SEVERPAY_TELEGRAM_EMOJI", "string", "Telegram button emoji",
|
||||
"Emoji prepended to the Telegram bot payment button when customized.",
|
||||
"💳", "TELEGRAM_EMOJI"),
|
||||
(
|
||||
"PAYMENT_SEVERPAY_WEBAPP_LABEL_RU",
|
||||
"string",
|
||||
"WebApp button text (RU)",
|
||||
"Custom Russian text shown in the Web App payment method button.",
|
||||
"",
|
||||
"WEBAPP_LABEL_RU",
|
||||
),
|
||||
(
|
||||
"PAYMENT_SEVERPAY_WEBAPP_LABEL_EN",
|
||||
"string",
|
||||
"WebApp button text (EN)",
|
||||
"Custom English text shown in the Web App payment method button.",
|
||||
"",
|
||||
"WEBAPP_LABEL_EN",
|
||||
),
|
||||
(
|
||||
"PAYMENT_SEVERPAY_WEBAPP_ICON",
|
||||
"icon",
|
||||
"WebApp button icon",
|
||||
"Lucide icon name rendered inside the Web App payment method button.",
|
||||
"CreditCard",
|
||||
"WEBAPP_ICON",
|
||||
),
|
||||
(
|
||||
"PAYMENT_SEVERPAY_TELEGRAM_LABEL_RU",
|
||||
"string",
|
||||
"Telegram button text (RU)",
|
||||
"Custom Russian text shown in Telegram bot payment buttons.",
|
||||
"",
|
||||
"TELEGRAM_LABEL_RU",
|
||||
),
|
||||
(
|
||||
"PAYMENT_SEVERPAY_TELEGRAM_LABEL_EN",
|
||||
"string",
|
||||
"Telegram button text (EN)",
|
||||
"Custom English text shown in Telegram bot payment buttons.",
|
||||
"",
|
||||
"TELEGRAM_LABEL_EN",
|
||||
),
|
||||
(
|
||||
"PAYMENT_SEVERPAY_TELEGRAM_EMOJI",
|
||||
"string",
|
||||
"Telegram button emoji",
|
||||
"Emoji prepended to the Telegram bot payment button when customized.",
|
||||
"💳",
|
||||
"TELEGRAM_EMOJI",
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
_CONFIG_MANIFEST = (
|
||||
ProviderManifestField("SEVERPAY_ENABLED", "bool", "Включена",
|
||||
subsection="SeverPay", attr="ENABLED"),
|
||||
ProviderManifestField(
|
||||
"SEVERPAY_ENABLED", "bool", "Включена", subsection="SeverPay", attr="ENABLED"
|
||||
),
|
||||
ProviderManifestField("SEVERPAY_MID", "int", "MID", subsection="SeverPay", attr="MID"),
|
||||
ProviderManifestField("SEVERPAY_TOKEN", "string", "Token", subsection="SeverPay",
|
||||
secret=True, attr="TOKEN"),
|
||||
ProviderManifestField("SEVERPAY_BASE_URL", "url", "Base URL",
|
||||
placeholder="https://severpay.io/api/merchant",
|
||||
subsection="SeverPay", attr="BASE_URL"),
|
||||
ProviderManifestField("SEVERPAY_RETURN_URL", "url", "Return URL",
|
||||
subsection="SeverPay", attr="RETURN_URL"),
|
||||
ProviderManifestField("SEVERPAY_LIFETIME_MINUTES", "int", "Payment link lifetime (minutes)",
|
||||
description="30..4320; leave empty for the SeverPay default.",
|
||||
subsection="SeverPay", min=30, max=4320, attr="LIFETIME_MINUTES"),
|
||||
ProviderManifestField(
|
||||
"SEVERPAY_TOKEN", "string", "Token", subsection="SeverPay", secret=True, attr="TOKEN"
|
||||
),
|
||||
ProviderManifestField(
|
||||
"SEVERPAY_BASE_URL",
|
||||
"url",
|
||||
"Base URL",
|
||||
placeholder="https://severpay.io/api/merchant",
|
||||
subsection="SeverPay",
|
||||
attr="BASE_URL",
|
||||
),
|
||||
ProviderManifestField(
|
||||
"SEVERPAY_RETURN_URL", "url", "Return URL", subsection="SeverPay", attr="RETURN_URL"
|
||||
),
|
||||
ProviderManifestField(
|
||||
"SEVERPAY_LIFETIME_MINUTES",
|
||||
"int",
|
||||
"Payment link lifetime (minutes)",
|
||||
description="30..4320; leave empty for the SeverPay default.",
|
||||
subsection="SeverPay",
|
||||
min=30,
|
||||
max=4320,
|
||||
attr="LIFETIME_MINUTES",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -125,11 +125,7 @@ def payment_link_message_text(
|
||||
"topup",
|
||||
"premium_topup",
|
||||
}
|
||||
key = (
|
||||
"payment_link_message_traffic"
|
||||
if traffic_like
|
||||
else "payment_link_message"
|
||||
)
|
||||
key = "payment_link_message_traffic" if traffic_like else "payment_link_message"
|
||||
body = translator(
|
||||
key,
|
||||
months=int(parts.months),
|
||||
|
||||
@@ -35,9 +35,7 @@ async def resolve_user_language(
|
||||
if db_user is None:
|
||||
db_user = await user_dal.get_user_by_id(session, user_id)
|
||||
language = (
|
||||
db_user.language_code
|
||||
if db_user and db_user.language_code
|
||||
else settings.DEFAULT_LANGUAGE
|
||||
db_user.language_code if db_user and db_user.language_code else settings.DEFAULT_LANGUAGE
|
||||
)
|
||||
return db_user, language
|
||||
|
||||
@@ -256,9 +254,7 @@ async def finalize_successful_payment(
|
||||
activation_months = (
|
||||
int(float(req.months)) if is_subscription else int(float(req.traffic_amount or req.months))
|
||||
)
|
||||
traffic_gb_for_activation = (
|
||||
float(req.traffic_amount or req.months) if is_traffic else None
|
||||
)
|
||||
traffic_gb_for_activation = float(req.traffic_amount or req.months) if is_traffic else None
|
||||
|
||||
try:
|
||||
activation = await req.subscription_service.activate_subscription(
|
||||
@@ -308,9 +304,7 @@ async def finalize_successful_payment(
|
||||
base_end_date = activation.get("end_date") if activation else None
|
||||
final_end_date = base_end_date
|
||||
applied_referee_bonus_days = 0
|
||||
applied_promo_bonus_days = (
|
||||
activation.get("applied_promo_bonus_days", 0) if activation else 0
|
||||
)
|
||||
applied_promo_bonus_days = activation.get("applied_promo_bonus_days", 0) if activation else 0
|
||||
|
||||
inviter_name: Optional[str] = None
|
||||
if referral_bonus and referral_bonus.get("referee_new_end_date"):
|
||||
@@ -351,9 +345,7 @@ async def finalize_successful_payment(
|
||||
log_prefix=req.log_prefix,
|
||||
)
|
||||
|
||||
refreshed_payment = await payment_dal.get_payment_by_db_id(
|
||||
req.session, req.payment.payment_id
|
||||
)
|
||||
refreshed_payment = await payment_dal.get_payment_by_db_id(req.session, req.payment.payment_id)
|
||||
tariff_key = getattr(refreshed_payment or req.payment, "tariff_key", None)
|
||||
|
||||
await notify_admins_payment_received(
|
||||
|
||||
@@ -36,9 +36,7 @@ async def lookup_payment_by_order_or_provider_id(
|
||||
if payment_db_id is not None:
|
||||
payment = await payment_dal.get_payment_by_db_id(session, payment_db_id)
|
||||
if not payment and provider_payment_id:
|
||||
payment = await payment_dal.get_payment_by_provider_payment_id(
|
||||
session, provider_payment_id
|
||||
)
|
||||
payment = await payment_dal.get_payment_by_provider_payment_id(session, provider_payment_id)
|
||||
return payment
|
||||
|
||||
|
||||
@@ -54,9 +52,7 @@ async def notify_user_payment_failed(
|
||||
"""Send the localized ``payment_failed`` text to the user; never raises."""
|
||||
db_user = payment.user or await user_dal.get_user_by_id(session, payment.user_id)
|
||||
language = (
|
||||
db_user.language_code
|
||||
if db_user and db_user.language_code
|
||||
else settings.DEFAULT_LANGUAGE
|
||||
db_user.language_code if db_user and db_user.language_code else settings.DEFAULT_LANGUAGE
|
||||
)
|
||||
translator = make_translator(i18n, language)
|
||||
try:
|
||||
|
||||
@@ -323,9 +323,7 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
|
||||
provider="telegram_stars",
|
||||
)
|
||||
payload_units = amounts.purchased_gb if amounts.traffic_sale else ctx.months
|
||||
payload = (
|
||||
f"{payment.payment_id}:{format_number_for_payload(payload_units)}:{ctx.sale_mode}"
|
||||
)
|
||||
payload = f"{payment.payment_id}:{format_number_for_payload(payload_units)}:{ctx.sale_mode}"
|
||||
prices = [LabeledPrice(label=ctx.description, amount=ctx.stars_price)]
|
||||
create_invoice_link = getattr(bot, "create_invoice_link", None)
|
||||
if callable(create_invoice_link):
|
||||
@@ -371,25 +369,64 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
|
||||
|
||||
_PRESENTATION_MANIFEST = tuple(
|
||||
ProviderManifestField(
|
||||
key=key, type=type_, label=label, description=description,
|
||||
placeholder=placeholder, subsection="Telegram Stars",
|
||||
target="presentation", attr=attr,
|
||||
key=key,
|
||||
type=type_,
|
||||
label=label,
|
||||
description=description,
|
||||
placeholder=placeholder,
|
||||
subsection="Telegram Stars",
|
||||
target="presentation",
|
||||
attr=attr,
|
||||
)
|
||||
for key, type_, label, description, placeholder, attr in (
|
||||
("PAYMENT_STARS_WEBAPP_LABEL_RU", "string", "WebApp button text (RU)",
|
||||
"Custom Russian text shown in the Web App payment method button.", "", "WEBAPP_LABEL_RU"),
|
||||
("PAYMENT_STARS_WEBAPP_LABEL_EN", "string", "WebApp button text (EN)",
|
||||
"Custom English text shown in the Web App payment method button.", "", "WEBAPP_LABEL_EN"),
|
||||
("PAYMENT_STARS_WEBAPP_ICON", "icon", "WebApp button icon",
|
||||
"Lucide icon name rendered inside the Web App payment method button.",
|
||||
"Sparkles", "WEBAPP_ICON"),
|
||||
("PAYMENT_STARS_TELEGRAM_LABEL_RU", "string", "Telegram button text (RU)",
|
||||
"Custom Russian text shown in Telegram bot payment buttons.", "", "TELEGRAM_LABEL_RU"),
|
||||
("PAYMENT_STARS_TELEGRAM_LABEL_EN", "string", "Telegram button text (EN)",
|
||||
"Custom English text shown in Telegram bot payment buttons.", "", "TELEGRAM_LABEL_EN"),
|
||||
("PAYMENT_STARS_TELEGRAM_EMOJI", "string", "Telegram button emoji",
|
||||
"Emoji prepended to the Telegram bot payment button when customized.",
|
||||
"🌟", "TELEGRAM_EMOJI"),
|
||||
(
|
||||
"PAYMENT_STARS_WEBAPP_LABEL_RU",
|
||||
"string",
|
||||
"WebApp button text (RU)",
|
||||
"Custom Russian text shown in the Web App payment method button.",
|
||||
"",
|
||||
"WEBAPP_LABEL_RU",
|
||||
),
|
||||
(
|
||||
"PAYMENT_STARS_WEBAPP_LABEL_EN",
|
||||
"string",
|
||||
"WebApp button text (EN)",
|
||||
"Custom English text shown in the Web App payment method button.",
|
||||
"",
|
||||
"WEBAPP_LABEL_EN",
|
||||
),
|
||||
(
|
||||
"PAYMENT_STARS_WEBAPP_ICON",
|
||||
"icon",
|
||||
"WebApp button icon",
|
||||
"Lucide icon name rendered inside the Web App payment method button.",
|
||||
"Sparkles",
|
||||
"WEBAPP_ICON",
|
||||
),
|
||||
(
|
||||
"PAYMENT_STARS_TELEGRAM_LABEL_RU",
|
||||
"string",
|
||||
"Telegram button text (RU)",
|
||||
"Custom Russian text shown in Telegram bot payment buttons.",
|
||||
"",
|
||||
"TELEGRAM_LABEL_RU",
|
||||
),
|
||||
(
|
||||
"PAYMENT_STARS_TELEGRAM_LABEL_EN",
|
||||
"string",
|
||||
"Telegram button text (EN)",
|
||||
"Custom English text shown in Telegram bot payment buttons.",
|
||||
"",
|
||||
"TELEGRAM_LABEL_EN",
|
||||
),
|
||||
(
|
||||
"PAYMENT_STARS_TELEGRAM_EMOJI",
|
||||
"string",
|
||||
"Telegram button emoji",
|
||||
"Emoji prepended to the Telegram bot payment button when customized.",
|
||||
"🌟",
|
||||
"TELEGRAM_EMOJI",
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -542,50 +542,120 @@ def create_service(ctx: ServiceFactoryContext) -> WataService:
|
||||
|
||||
_PRESENTATION_MANIFEST = tuple(
|
||||
ProviderManifestField(
|
||||
key=key, type=type_, label=label, description=description,
|
||||
placeholder=placeholder, subsection="Wata",
|
||||
target="presentation", attr=attr,
|
||||
key=key,
|
||||
type=type_,
|
||||
label=label,
|
||||
description=description,
|
||||
placeholder=placeholder,
|
||||
subsection="Wata",
|
||||
target="presentation",
|
||||
attr=attr,
|
||||
)
|
||||
for key, type_, label, description, placeholder, attr in (
|
||||
("PAYMENT_WATA_WEBAPP_LABEL_RU", "string", "WebApp button text (RU)",
|
||||
"Custom Russian text shown in the Web App payment method button.", "", "WEBAPP_LABEL_RU"),
|
||||
("PAYMENT_WATA_WEBAPP_LABEL_EN", "string", "WebApp button text (EN)",
|
||||
"Custom English text shown in the Web App payment method button.", "", "WEBAPP_LABEL_EN"),
|
||||
("PAYMENT_WATA_WEBAPP_ICON", "icon", "WebApp button icon",
|
||||
"Lucide icon name rendered inside the Web App payment method button.",
|
||||
"WalletCards", "WEBAPP_ICON"),
|
||||
("PAYMENT_WATA_TELEGRAM_LABEL_RU", "string", "Telegram button text (RU)",
|
||||
"Custom Russian text shown in Telegram bot payment buttons.", "", "TELEGRAM_LABEL_RU"),
|
||||
("PAYMENT_WATA_TELEGRAM_LABEL_EN", "string", "Telegram button text (EN)",
|
||||
"Custom English text shown in Telegram bot payment buttons.", "", "TELEGRAM_LABEL_EN"),
|
||||
("PAYMENT_WATA_TELEGRAM_EMOJI", "string", "Telegram button emoji",
|
||||
"Emoji prepended to the Telegram bot payment button when customized.",
|
||||
"💳", "TELEGRAM_EMOJI"),
|
||||
(
|
||||
"PAYMENT_WATA_WEBAPP_LABEL_RU",
|
||||
"string",
|
||||
"WebApp button text (RU)",
|
||||
"Custom Russian text shown in the Web App payment method button.",
|
||||
"",
|
||||
"WEBAPP_LABEL_RU",
|
||||
),
|
||||
(
|
||||
"PAYMENT_WATA_WEBAPP_LABEL_EN",
|
||||
"string",
|
||||
"WebApp button text (EN)",
|
||||
"Custom English text shown in the Web App payment method button.",
|
||||
"",
|
||||
"WEBAPP_LABEL_EN",
|
||||
),
|
||||
(
|
||||
"PAYMENT_WATA_WEBAPP_ICON",
|
||||
"icon",
|
||||
"WebApp button icon",
|
||||
"Lucide icon name rendered inside the Web App payment method button.",
|
||||
"WalletCards",
|
||||
"WEBAPP_ICON",
|
||||
),
|
||||
(
|
||||
"PAYMENT_WATA_TELEGRAM_LABEL_RU",
|
||||
"string",
|
||||
"Telegram button text (RU)",
|
||||
"Custom Russian text shown in Telegram bot payment buttons.",
|
||||
"",
|
||||
"TELEGRAM_LABEL_RU",
|
||||
),
|
||||
(
|
||||
"PAYMENT_WATA_TELEGRAM_LABEL_EN",
|
||||
"string",
|
||||
"Telegram button text (EN)",
|
||||
"Custom English text shown in Telegram bot payment buttons.",
|
||||
"",
|
||||
"TELEGRAM_LABEL_EN",
|
||||
),
|
||||
(
|
||||
"PAYMENT_WATA_TELEGRAM_EMOJI",
|
||||
"string",
|
||||
"Telegram button emoji",
|
||||
"Emoji prepended to the Telegram bot payment button when customized.",
|
||||
"💳",
|
||||
"TELEGRAM_EMOJI",
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
_CONFIG_MANIFEST = (
|
||||
ProviderManifestField("WATA_ENABLED", "bool", "Enabled", subsection="Wata", attr="ENABLED"),
|
||||
ProviderManifestField("WATA_API_TOKEN", "string", "API token", subsection="Wata",
|
||||
secret=True, attr="API_TOKEN"),
|
||||
ProviderManifestField("WATA_BASE_URL", "url", "Base URL",
|
||||
placeholder="https://api.wata.pro/api/h2h",
|
||||
subsection="Wata", attr="BASE_URL"),
|
||||
ProviderManifestField("WATA_RETURN_URL", "url", "Return URL",
|
||||
subsection="Wata", attr="RETURN_URL"),
|
||||
ProviderManifestField("WATA_FAILED_URL", "url", "Failed URL",
|
||||
subsection="Wata", attr="FAILED_URL"),
|
||||
ProviderManifestField("WATA_PAYMENT_LINK_TTL_DAYS", "int", "Payment link lifetime (days)",
|
||||
description="1..30; Wata defaults to 3 days and allows up to 30 days.",
|
||||
subsection="Wata", min=1, max=30, attr="PAYMENT_LINK_TTL_DAYS"),
|
||||
ProviderManifestField("WATA_WEBHOOK_VERIFY_SIGNATURE", "bool", "Verify webhook signature",
|
||||
subsection="Wata", attr="WEBHOOK_VERIFY_SIGNATURE"),
|
||||
ProviderManifestField("WATA_PUBLIC_KEY", "text", "Webhook public key",
|
||||
description="Optional. If empty, the backend fetches it from Wata.",
|
||||
subsection="Wata", secret=True, attr="PUBLIC_KEY"),
|
||||
ProviderManifestField("WATA_TRUSTED_IPS", "string", "Trusted IPs",
|
||||
description="Comma-separated IP addresses accepted for Wata webhooks.",
|
||||
subsection="Wata", attr="TRUSTED_IPS"),
|
||||
ProviderManifestField(
|
||||
"WATA_API_TOKEN", "string", "API token", subsection="Wata", secret=True, attr="API_TOKEN"
|
||||
),
|
||||
ProviderManifestField(
|
||||
"WATA_BASE_URL",
|
||||
"url",
|
||||
"Base URL",
|
||||
placeholder="https://api.wata.pro/api/h2h",
|
||||
subsection="Wata",
|
||||
attr="BASE_URL",
|
||||
),
|
||||
ProviderManifestField(
|
||||
"WATA_RETURN_URL", "url", "Return URL", subsection="Wata", attr="RETURN_URL"
|
||||
),
|
||||
ProviderManifestField(
|
||||
"WATA_FAILED_URL", "url", "Failed URL", subsection="Wata", attr="FAILED_URL"
|
||||
),
|
||||
ProviderManifestField(
|
||||
"WATA_PAYMENT_LINK_TTL_DAYS",
|
||||
"int",
|
||||
"Payment link lifetime (days)",
|
||||
description="1..30; Wata defaults to 3 days and allows up to 30 days.",
|
||||
subsection="Wata",
|
||||
min=1,
|
||||
max=30,
|
||||
attr="PAYMENT_LINK_TTL_DAYS",
|
||||
),
|
||||
ProviderManifestField(
|
||||
"WATA_WEBHOOK_VERIFY_SIGNATURE",
|
||||
"bool",
|
||||
"Verify webhook signature",
|
||||
subsection="Wata",
|
||||
attr="WEBHOOK_VERIFY_SIGNATURE",
|
||||
),
|
||||
ProviderManifestField(
|
||||
"WATA_PUBLIC_KEY",
|
||||
"text",
|
||||
"Webhook public key",
|
||||
description="Optional. If empty, the backend fetches it from Wata.",
|
||||
subsection="Wata",
|
||||
secret=True,
|
||||
attr="PUBLIC_KEY",
|
||||
),
|
||||
ProviderManifestField(
|
||||
"WATA_TRUSTED_IPS",
|
||||
"string",
|
||||
"Trusted IPs",
|
||||
description="Comma-separated IP addresses accepted for Wata webhooks.",
|
||||
subsection="Wata",
|
||||
attr="TRUSTED_IPS",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -90,9 +90,7 @@ class YooKassaConfig(ProviderEnvConfig):
|
||||
AUTOPAYMENTS_ENABLED: bool = Field(default=False)
|
||||
AUTOPAYMENTS_REQUIRE_CARD_BINDING: bool = Field(default=True)
|
||||
|
||||
@field_validator(
|
||||
"SHOP_ID", "SECRET_KEY", "RETURN_URL", "DEFAULT_RECEIPT_EMAIL", mode="before"
|
||||
)
|
||||
@field_validator("SHOP_ID", "SECRET_KEY", "RETURN_URL", "DEFAULT_RECEIPT_EMAIL", mode="before")
|
||||
@classmethod
|
||||
def _strip_optional(cls, v):
|
||||
if isinstance(v, str) and not v.strip():
|
||||
@@ -147,7 +145,9 @@ class YooKassaService:
|
||||
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
|
||||
self._sdk_configured_for = (
|
||||
None # (shop_id, secret_key) currently loaded into the global SDK
|
||||
)
|
||||
|
||||
if not self.configured:
|
||||
if not self.config.ENABLED:
|
||||
@@ -1214,9 +1214,7 @@ async def _initiate_yk_payment(
|
||||
"description": payment_description,
|
||||
"subscription_duration_months": int(months) if sale_base == "subscription" else None,
|
||||
"sale_mode": sale_base,
|
||||
"tariff_key": sale_mode.split("@", 1)[1].split("|", 1)[0]
|
||||
if "@" in sale_mode
|
||||
else None,
|
||||
"tariff_key": sale_mode.split("@", 1)[1].split("|", 1)[0] if "@" in sale_mode else None,
|
||||
"purchased_gb": float(months)
|
||||
if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"}
|
||||
else None,
|
||||
@@ -2505,7 +2503,9 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
def create_service(ctx: ServiceFactoryContext) -> YooKassaService:
|
||||
bundle = ctx.config_for("yookassa_service")
|
||||
config = bundle.config if bundle and isinstance(bundle.config, YooKassaConfig) else YooKassaConfig()
|
||||
config = (
|
||||
bundle.config if bundle and isinstance(bundle.config, YooKassaConfig) else YooKassaConfig()
|
||||
)
|
||||
return YooKassaService(
|
||||
shop_id=config.SHOP_ID,
|
||||
secret_key=config.SECRET_KEY,
|
||||
@@ -2516,12 +2516,7 @@ def create_service(ctx: ServiceFactoryContext) -> YooKassaService:
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
|
||||
settings = ctx.request.app["settings"]
|
||||
service: YooKassaService = ctx.request.app["yookassa_service"]
|
||||
if not service or not service.configured:
|
||||
return payment_unavailable()
|
||||
@@ -2588,49 +2583,116 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
|
||||
|
||||
_PRESENTATION_MANIFEST = tuple(
|
||||
ProviderManifestField(
|
||||
key=key, type=type_, label=label, description=description,
|
||||
placeholder=placeholder, subsection="YooKassa",
|
||||
target="presentation", attr=attr,
|
||||
key=key,
|
||||
type=type_,
|
||||
label=label,
|
||||
description=description,
|
||||
placeholder=placeholder,
|
||||
subsection="YooKassa",
|
||||
target="presentation",
|
||||
attr=attr,
|
||||
)
|
||||
for key, type_, label, description, placeholder, attr in (
|
||||
("PAYMENT_YOOKASSA_WEBAPP_LABEL_RU", "string", "WebApp button text (RU)",
|
||||
"Custom Russian text shown in the Web App payment method button.", "", "WEBAPP_LABEL_RU"),
|
||||
("PAYMENT_YOOKASSA_WEBAPP_LABEL_EN", "string", "WebApp button text (EN)",
|
||||
"Custom English text shown in the Web App payment method button.", "", "WEBAPP_LABEL_EN"),
|
||||
("PAYMENT_YOOKASSA_WEBAPP_ICON", "icon", "WebApp button icon",
|
||||
"Lucide icon name rendered inside the Web App payment method button.",
|
||||
"CreditCard", "WEBAPP_ICON"),
|
||||
("PAYMENT_YOOKASSA_TELEGRAM_LABEL_RU", "string", "Telegram button text (RU)",
|
||||
"Custom Russian text shown in Telegram bot payment buttons.", "", "TELEGRAM_LABEL_RU"),
|
||||
("PAYMENT_YOOKASSA_TELEGRAM_LABEL_EN", "string", "Telegram button text (EN)",
|
||||
"Custom English text shown in Telegram bot payment buttons.", "", "TELEGRAM_LABEL_EN"),
|
||||
("PAYMENT_YOOKASSA_TELEGRAM_EMOJI", "string", "Telegram button emoji",
|
||||
"Emoji prepended to the Telegram bot payment button when customized.",
|
||||
"💳", "TELEGRAM_EMOJI"),
|
||||
(
|
||||
"PAYMENT_YOOKASSA_WEBAPP_LABEL_RU",
|
||||
"string",
|
||||
"WebApp button text (RU)",
|
||||
"Custom Russian text shown in the Web App payment method button.",
|
||||
"",
|
||||
"WEBAPP_LABEL_RU",
|
||||
),
|
||||
(
|
||||
"PAYMENT_YOOKASSA_WEBAPP_LABEL_EN",
|
||||
"string",
|
||||
"WebApp button text (EN)",
|
||||
"Custom English text shown in the Web App payment method button.",
|
||||
"",
|
||||
"WEBAPP_LABEL_EN",
|
||||
),
|
||||
(
|
||||
"PAYMENT_YOOKASSA_WEBAPP_ICON",
|
||||
"icon",
|
||||
"WebApp button icon",
|
||||
"Lucide icon name rendered inside the Web App payment method button.",
|
||||
"CreditCard",
|
||||
"WEBAPP_ICON",
|
||||
),
|
||||
(
|
||||
"PAYMENT_YOOKASSA_TELEGRAM_LABEL_RU",
|
||||
"string",
|
||||
"Telegram button text (RU)",
|
||||
"Custom Russian text shown in Telegram bot payment buttons.",
|
||||
"",
|
||||
"TELEGRAM_LABEL_RU",
|
||||
),
|
||||
(
|
||||
"PAYMENT_YOOKASSA_TELEGRAM_LABEL_EN",
|
||||
"string",
|
||||
"Telegram button text (EN)",
|
||||
"Custom English text shown in Telegram bot payment buttons.",
|
||||
"",
|
||||
"TELEGRAM_LABEL_EN",
|
||||
),
|
||||
(
|
||||
"PAYMENT_YOOKASSA_TELEGRAM_EMOJI",
|
||||
"string",
|
||||
"Telegram button emoji",
|
||||
"Emoji prepended to the Telegram bot payment button when customized.",
|
||||
"💳",
|
||||
"TELEGRAM_EMOJI",
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
_CONFIG_MANIFEST = (
|
||||
ProviderManifestField("YOOKASSA_ENABLED", "bool", "Включена",
|
||||
subsection="YooKassa", attr="ENABLED"),
|
||||
ProviderManifestField("YOOKASSA_SHOP_ID", "string", "Shop ID",
|
||||
subsection="YooKassa", attr="SHOP_ID"),
|
||||
ProviderManifestField("YOOKASSA_SECRET_KEY", "string", "Secret key",
|
||||
subsection="YooKassa", secret=True, attr="SECRET_KEY"),
|
||||
ProviderManifestField("YOOKASSA_RETURN_URL", "url", "Return URL",
|
||||
subsection="YooKassa", attr="RETURN_URL"),
|
||||
ProviderManifestField("YOOKASSA_DEFAULT_RECEIPT_EMAIL", "string",
|
||||
"Email для чека по умолчанию",
|
||||
subsection="YooKassa", attr="DEFAULT_RECEIPT_EMAIL"),
|
||||
ProviderManifestField("YOOKASSA_VAT_CODE", "int", "VAT code",
|
||||
description="1..6 в зависимости от системы налогообложения",
|
||||
subsection="YooKassa", min=1, max=6, attr="VAT_CODE"),
|
||||
ProviderManifestField("YOOKASSA_AUTOPAYMENTS_ENABLED", "bool",
|
||||
"Автоплатежи (recurring)",
|
||||
subsection="YooKassa", attr="AUTOPAYMENTS_ENABLED"),
|
||||
ProviderManifestField("YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING", "bool",
|
||||
"Принудительная привязка карты",
|
||||
subsection="YooKassa", attr="AUTOPAYMENTS_REQUIRE_CARD_BINDING"),
|
||||
ProviderManifestField(
|
||||
"YOOKASSA_ENABLED", "bool", "Включена", subsection="YooKassa", attr="ENABLED"
|
||||
),
|
||||
ProviderManifestField(
|
||||
"YOOKASSA_SHOP_ID", "string", "Shop ID", subsection="YooKassa", attr="SHOP_ID"
|
||||
),
|
||||
ProviderManifestField(
|
||||
"YOOKASSA_SECRET_KEY",
|
||||
"string",
|
||||
"Secret key",
|
||||
subsection="YooKassa",
|
||||
secret=True,
|
||||
attr="SECRET_KEY",
|
||||
),
|
||||
ProviderManifestField(
|
||||
"YOOKASSA_RETURN_URL", "url", "Return URL", subsection="YooKassa", attr="RETURN_URL"
|
||||
),
|
||||
ProviderManifestField(
|
||||
"YOOKASSA_DEFAULT_RECEIPT_EMAIL",
|
||||
"string",
|
||||
"Email для чека по умолчанию",
|
||||
subsection="YooKassa",
|
||||
attr="DEFAULT_RECEIPT_EMAIL",
|
||||
),
|
||||
ProviderManifestField(
|
||||
"YOOKASSA_VAT_CODE",
|
||||
"int",
|
||||
"VAT code",
|
||||
description="1..6 в зависимости от системы налогообложения",
|
||||
subsection="YooKassa",
|
||||
min=1,
|
||||
max=6,
|
||||
attr="VAT_CODE",
|
||||
),
|
||||
ProviderManifestField(
|
||||
"YOOKASSA_AUTOPAYMENTS_ENABLED",
|
||||
"bool",
|
||||
"Автоплатежи (recurring)",
|
||||
subsection="YooKassa",
|
||||
attr="AUTOPAYMENTS_ENABLED",
|
||||
),
|
||||
ProviderManifestField(
|
||||
"YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING",
|
||||
"bool",
|
||||
"Принудительная привязка карты",
|
||||
subsection="YooKassa",
|
||||
attr="AUTOPAYMENTS_REQUIRE_CARD_BINDING",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -514,7 +514,7 @@ def _support_email(
|
||||
footer = _t_html(_resolve_i18n(i18n), lang, "email_footer_auto", brand=brand)
|
||||
preview_block = (
|
||||
f'<div style="margin:0 0 16px 0;background:{_BG};border:1px solid {_BORDER};'
|
||||
f'border-radius:14px;padding:14px 16px;font-size:14px;line-height:1.55;color:{_TEXT};'
|
||||
f"border-radius:14px;padding:14px 16px;font-size:14px;line-height:1.55;color:{_TEXT};"
|
||||
f'white-space:pre-wrap;">{html.escape(body_preview or "")}</div>'
|
||||
)
|
||||
body_parts = [_info_rows_html(rows), preview_block]
|
||||
|
||||
@@ -290,9 +290,7 @@ class NotificationService:
|
||||
return bool(value)
|
||||
|
||||
async def support_admin_email_notifications_enabled(self) -> bool:
|
||||
enabled = bool(
|
||||
getattr(self.settings, SUPPORT_ADMIN_EMAIL_NOTIFICATIONS_KEY, False)
|
||||
)
|
||||
enabled = bool(getattr(self.settings, SUPPORT_ADMIN_EMAIL_NOTIFICATIONS_KEY, False))
|
||||
if not self.session_factory:
|
||||
return enabled
|
||||
try:
|
||||
|
||||
@@ -255,8 +255,7 @@ class PanelWebhookService:
|
||||
"panel",
|
||||
{"event": event_name, "user": user_data},
|
||||
event_id=(
|
||||
f"{event_name}:"
|
||||
f"{telegram_id or user_data.get('uuid') or user_data.get('shortUuid')}"
|
||||
f"{event_name}:{telegram_id or user_data.get('uuid') or user_data.get('shortUuid')}"
|
||||
),
|
||||
)
|
||||
if not queued:
|
||||
|
||||
@@ -70,10 +70,9 @@ def _apply_to_provider_bundle(key: str, value: Any) -> bool:
|
||||
from bot.payment_providers import (
|
||||
find_manifest_owner,
|
||||
get_provider_bundle,
|
||||
get_spec_presentation,
|
||||
)
|
||||
|
||||
from bot.payment_providers import get_spec_presentation
|
||||
|
||||
owner = find_manifest_owner(key)
|
||||
if owner is None:
|
||||
return False
|
||||
@@ -213,9 +212,7 @@ async def load_overrides_from_db(settings: Settings, async_session_factory: sess
|
||||
overrides = await app_settings_dal.get_all_overrides(session)
|
||||
backup_overrides = _read_appearance_backup()
|
||||
missing_backup_overrides = {
|
||||
key: value
|
||||
for key, value in backup_overrides.items()
|
||||
if key not in overrides
|
||||
key: value for key, value in backup_overrides.items() if key not in overrides
|
||||
}
|
||||
if missing_backup_overrides:
|
||||
for key, value in missing_backup_overrides.items():
|
||||
|
||||
@@ -644,12 +644,12 @@ class TariffTrafficWorker:
|
||||
for node_uuid in node_uuids:
|
||||
stats_cache_key = (node_uuid, start_date, end_date)
|
||||
if stats_cache_key not in self._premium_node_stats_tick_cache:
|
||||
self._premium_node_stats_tick_cache[stats_cache_key] = (
|
||||
await self.panel_service.get_node_users_bandwidth_stats(
|
||||
node_uuid,
|
||||
start=start_date,
|
||||
end=end_date,
|
||||
)
|
||||
self._premium_node_stats_tick_cache[
|
||||
stats_cache_key
|
||||
] = await self.panel_service.get_node_users_bandwidth_stats(
|
||||
node_uuid,
|
||||
start=start_date,
|
||||
end=end_date,
|
||||
)
|
||||
stats = self._premium_node_stats_tick_cache.get(stats_cache_key)
|
||||
if not stats:
|
||||
|
||||
@@ -12,6 +12,15 @@ from config.webapp_themes_config import (
|
||||
resolved_webapp_themes_catalog,
|
||||
)
|
||||
|
||||
DEFAULT_SUBSCRIPTION_PURCHASE_DESCRIPTION_RU = (
|
||||
"Покупая или продлевая подписку, вы получаете доступ к VPN/прокси-сервису, "
|
||||
"который помогает защищать ваше соединение и поддерживать стабильный доступ к сети."
|
||||
)
|
||||
DEFAULT_SUBSCRIPTION_PURCHASE_DESCRIPTION_EN = (
|
||||
"By buying or renewing a subscription, you get access to a VPN/proxy service "
|
||||
"that helps protect your connection and keep your access stable."
|
||||
)
|
||||
|
||||
|
||||
def _split_csv(value: Optional[str]) -> List[str]:
|
||||
if not value:
|
||||
@@ -147,6 +156,18 @@ class Settings(BaseSettings):
|
||||
default=None,
|
||||
description="Comma-separated list of payment methods to show (e.g., severpay,wata,freekassa,yookassa,platega,stars,cryptopay)", # noqa: E501
|
||||
)
|
||||
SUBSCRIPTION_PURCHASE_DESCRIPTION_ENABLED: bool = Field(
|
||||
default=True,
|
||||
description="Show a localized description of the subscription before users choose a purchase/renewal period.", # noqa: E501
|
||||
)
|
||||
SUBSCRIPTION_PURCHASE_DESCRIPTION_RU: str = Field(
|
||||
default=DEFAULT_SUBSCRIPTION_PURCHASE_DESCRIPTION_RU,
|
||||
description="Russian subscription description shown before purchase/renewal options.",
|
||||
)
|
||||
SUBSCRIPTION_PURCHASE_DESCRIPTION_EN: str = Field(
|
||||
default=DEFAULT_SUBSCRIPTION_PURCHASE_DESCRIPTION_EN,
|
||||
description="English subscription description shown before purchase/renewal options.",
|
||||
)
|
||||
|
||||
MONTH_1_ENABLED: bool = Field(default=True, alias="1_MONTH_ENABLED")
|
||||
MONTH_3_ENABLED: bool = Field(default=True, alias="3_MONTHS_ENABLED")
|
||||
@@ -772,6 +793,22 @@ class Settings(BaseSettings):
|
||||
methods.append(sid)
|
||||
return methods or default_order
|
||||
|
||||
def subscription_purchase_description(self, language: Optional[str] = None) -> str:
|
||||
if not self.SUBSCRIPTION_PURCHASE_DESCRIPTION_ENABLED:
|
||||
return ""
|
||||
lang = (language or self.DEFAULT_LANGUAGE or "ru").split("-")[0].lower()
|
||||
primary = (
|
||||
self.SUBSCRIPTION_PURCHASE_DESCRIPTION_EN
|
||||
if lang == "en"
|
||||
else self.SUBSCRIPTION_PURCHASE_DESCRIPTION_RU
|
||||
)
|
||||
fallback = (
|
||||
self.SUBSCRIPTION_PURCHASE_DESCRIPTION_RU
|
||||
if lang == "en"
|
||||
else self.SUBSCRIPTION_PURCHASE_DESCRIPTION_EN
|
||||
)
|
||||
return (primary or fallback or "").strip()
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def email_auth_configured(self) -> bool:
|
||||
|
||||
@@ -738,8 +738,7 @@ def _migration_0022_add_indexes_for_admin_reports(connection: Connection) -> Non
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"CREATE INDEX IF NOT EXISTS ix_message_logs_timestamp "
|
||||
"ON message_logs (timestamp DESC)"
|
||||
"CREATE INDEX IF NOT EXISTS ix_message_logs_timestamp ON message_logs (timestamp DESC)"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -403,9 +403,7 @@ class SupportTicket(Base):
|
||||
passive_deletes=True,
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_support_tickets_status_last_msg", "status", "last_message_at"),
|
||||
)
|
||||
__table_args__ = (Index("ix_support_tickets_status_last_msg", "status", "last_message_at"),)
|
||||
|
||||
|
||||
class SupportTicketMessage(Base):
|
||||
|
||||
@@ -49,6 +49,8 @@ nano .env
|
||||
| Переменная | Назначение |
|
||||
| --- | --- |
|
||||
| `PAYMENT_METHODS_ORDER` | Порядок кнопок оплаты через запятую: `severpay`, `wata`, `freekassa`, `platega`, `yookassa`, `stars`, `cryptopay`. |
|
||||
| `SUBSCRIPTION_PURCHASE_DESCRIPTION_ENABLED` | Показывать описание подписки перед выбором срока покупки или продления в Telegram и Web App. |
|
||||
| `SUBSCRIPTION_PURCHASE_DESCRIPTION_RU` / `SUBSCRIPTION_PURCHASE_DESCRIPTION_EN` | Текст описания подписки для русской и английской локалей; эти же значения можно переопределить в админке. |
|
||||
| `PAYMENT_<METHOD>_WEBAPP_LABEL_RU` / `PAYMENT_<METHOD>_WEBAPP_LABEL_EN` / `PAYMENT_<METHOD>_WEBAPP_ICON` | Необязательная мультиязычная кастомизация текста и lucide-иконки кнопки оплаты в Web App. |
|
||||
| `PAYMENT_<METHOD>_TELEGRAM_LABEL_RU` / `PAYMENT_<METHOD>_TELEGRAM_LABEL_EN` / `PAYMENT_<METHOD>_TELEGRAM_EMOJI` | Необязательная мультиязычная кастомизация текста и эмодзи кнопки оплаты в Telegram-боте. |
|
||||
| `YOOKASSA_ENABLED` | Включает YooKassa. |
|
||||
|
||||
@@ -279,6 +279,9 @@
|
||||
$: plans = data?.plans?.length ? data.plans : DEV_MOCK.data.plans;
|
||||
$: methods = data?.payment_methods?.length ? data.payment_methods : [];
|
||||
$: appSettings = data?.settings || DEV_MOCK.data.settings;
|
||||
$: subscriptionPurchaseDescription = String(
|
||||
appSettings?.subscription_purchase_description || ""
|
||||
).trim();
|
||||
$: trafficMode = Boolean(appSettings?.traffic_mode);
|
||||
$: tariffMode = plans.some((plan) => plan?.tariff_key);
|
||||
$: tariffCatalog = buildTariffCatalog(plans);
|
||||
@@ -1321,6 +1324,7 @@
|
||||
{selectedTariffPlans}
|
||||
{singleTariffMode}
|
||||
{subscription}
|
||||
{subscriptionPurchaseDescription}
|
||||
{tariffCatalog}
|
||||
{tariffMode}
|
||||
closeDeviceDisconnectDialog={devicesStore.closeDeviceDisconnectDialog}
|
||||
|
||||
@@ -269,6 +269,14 @@
|
||||
value={valueFor(field) ?? ""}
|
||||
oninput={(e) => settingsStore.markDirty(field.key, e.currentTarget.value)}
|
||||
/>
|
||||
{:else if field.type === "text"}
|
||||
<textarea
|
||||
class="admin-setting-textarea"
|
||||
rows="4"
|
||||
placeholder={field.placeholder}
|
||||
value={valueFor(field) ?? ""}
|
||||
oninput={(e) => settingsStore.markDirty(field.key, e.currentTarget.value)}
|
||||
></textarea>
|
||||
{:else if field.secret}
|
||||
<input
|
||||
class="input"
|
||||
|
||||
@@ -669,6 +669,10 @@ export async function mockApi(path, options = {}, context = {}) {
|
||||
}
|
||||
const language = normalizeLangCode(payload?.language || currentLang);
|
||||
DEV_MOCK.data.user.language_code = language;
|
||||
DEV_MOCK.data.settings.subscription_purchase_description =
|
||||
language === "en"
|
||||
? "By buying or renewing a subscription, you get access to a VPN/proxy service that helps protect your connection and keep your access stable."
|
||||
: "Покупая или продлевая подписку, вы получаете доступ к VPN/прокси-сервису, который помогает защищать ваше соединение и поддерживать стабильный доступ к сети.";
|
||||
return { ok: true, language };
|
||||
}
|
||||
if (path === "/account/email/request" && String(options.method || "").toUpperCase() === "POST") {
|
||||
|
||||
@@ -222,6 +222,8 @@ export const DEV_MOCK = {
|
||||
trial_duration_days: 5,
|
||||
trial_traffic_limit_gb: 10,
|
||||
trial_traffic_strategy: "NO_RESET",
|
||||
subscription_purchase_description:
|
||||
"Покупая или продлевая подписку, вы получаете доступ к VPN/прокси-сервису, который помогает защищать ваше соединение и поддерживать стабильный доступ к сети.",
|
||||
email_auth_enabled: true,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -206,6 +206,7 @@
|
||||
|
||||
.admin-setting-control .input,
|
||||
.admin-setting-control .admin-setting-select,
|
||||
.admin-setting-control .admin-setting-textarea,
|
||||
.admin-setting-control input[type="text"],
|
||||
.admin-setting-control input[type="number"] {
|
||||
flex: 1 1 160px;
|
||||
@@ -213,6 +214,25 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.admin-setting-control .admin-setting-textarea {
|
||||
min-height: 86px;
|
||||
resize: vertical;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--admin-border-strong);
|
||||
background: color-mix(in srgb, var(--admin-bg) 82%, var(--admin-surface-2));
|
||||
color: var(--admin-text);
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
outline: none;
|
||||
transition: border-color 0.12s ease, box-shadow 0.12s ease;
|
||||
}
|
||||
|
||||
.admin-setting-control .admin-setting-textarea:focus {
|
||||
border-color: var(--admin-ring);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 20%, transparent);
|
||||
}
|
||||
|
||||
.admin-btn.admin-btn-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
|
||||
@@ -795,6 +795,24 @@ a {
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.subscription-purchase-description {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 11px 12px;
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 34%, var(--border));
|
||||
border-radius: var(--radius);
|
||||
background: color-mix(in srgb, var(--accent) 8%, var(--surface-muted));
|
||||
box-shadow: inset 0 1px 0 var(--inset-highlight);
|
||||
}
|
||||
|
||||
.subscription-purchase-description p {
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.skeleton-row,
|
||||
.skeleton-method,
|
||||
.skeleton-pay-button {
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
export let setPasswordValue = "";
|
||||
export let singleTariffMode = false;
|
||||
export let subscription = {};
|
||||
export let subscriptionPurchaseDescription = "";
|
||||
export let tariffCatalog = [];
|
||||
export let tariffMode = false;
|
||||
export let trafficMode = false;
|
||||
@@ -111,6 +112,13 @@
|
||||
return trafficMode ? t("wa_traffic_packages_choose") : t("wa_subscription_choose_period");
|
||||
}
|
||||
|
||||
function showSubscriptionPurchaseDescription() {
|
||||
if (!subscriptionPurchaseDescription || trafficMode) return false;
|
||||
if (!tariffMode) return true;
|
||||
if (paymentStep === "tariff") return false;
|
||||
return String(selectedTariff?.billing_model || "period").toLowerCase() !== "traffic";
|
||||
}
|
||||
|
||||
export let closeDeviceDisconnectDialog = () => {};
|
||||
export let closeLinkEmailDialog = () => {};
|
||||
export let closePaymentModal = () => {};
|
||||
@@ -184,6 +192,11 @@
|
||||
</p>
|
||||
{/if}
|
||||
{#if selectedTariffPlans.length}
|
||||
{#if showSubscriptionPurchaseDescription()}
|
||||
<div class="subscription-purchase-description">
|
||||
<p>{subscriptionPurchaseDescription}</p>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="period-grid period-grid-two-columns">
|
||||
{#each selectedTariffPlans as plan}
|
||||
<button
|
||||
@@ -233,6 +246,11 @@
|
||||
branch above, so users on legacy mode saw the period grid, payment
|
||||
method grid and pay button duplicated.
|
||||
-->
|
||||
{#if showSubscriptionPurchaseDescription()}
|
||||
<div class="subscription-purchase-description">
|
||||
<p>{subscriptionPurchaseDescription}</p>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="period-grid period-grid-two-columns">
|
||||
{#each plans as plan}
|
||||
<button
|
||||
|
||||
@@ -1311,6 +1311,12 @@
|
||||
"admin_settings_field_stars_traffic_packages_label": "Stars Traffic Packages",
|
||||
"admin_settings_field_payment_methods_order_label": "Payment Methods Order",
|
||||
"admin_settings_field_payment_methods_order_description": "Controls the 'Payment Methods Order' setting in admin overrides.",
|
||||
"admin_settings_field_subscription_purchase_description_enabled_label": "Show subscription description",
|
||||
"admin_settings_field_subscription_purchase_description_enabled_description": "Shows this text before the user chooses a purchase or renewal period.",
|
||||
"admin_settings_field_subscription_purchase_description_ru_label": "Subscription description (RU)",
|
||||
"admin_settings_field_subscription_purchase_description_ru_description": "Russian text shown during checkout.",
|
||||
"admin_settings_field_subscription_purchase_description_en_label": "Subscription description (EN)",
|
||||
"admin_settings_field_subscription_purchase_description_en_description": "English text shown during checkout.",
|
||||
"admin_settings_field_stars_enabled_label": "Stars Enabled",
|
||||
"admin_settings_field_yookassa_enabled_label": "YooKassa Enabled",
|
||||
"admin_settings_field_yookassa_shop_id_label": "YooKassa Shop ID",
|
||||
|
||||
@@ -1311,6 +1311,12 @@
|
||||
"admin_settings_field_stars_traffic_packages_label": "Пакеты трафика (Stars)",
|
||||
"admin_settings_field_payment_methods_order_label": "Порядок методов оплаты",
|
||||
"admin_settings_field_payment_methods_order_description": "Через запятую, например: severpay,freekassa,yookassa",
|
||||
"admin_settings_field_subscription_purchase_description_enabled_label": "Показывать описание подписки",
|
||||
"admin_settings_field_subscription_purchase_description_enabled_description": "Текст появится перед выбором срока покупки или продления.",
|
||||
"admin_settings_field_subscription_purchase_description_ru_label": "Описание подписки (RU)",
|
||||
"admin_settings_field_subscription_purchase_description_ru_description": "Русская версия текста на этапе оплаты.",
|
||||
"admin_settings_field_subscription_purchase_description_en_label": "Описание подписки (EN)",
|
||||
"admin_settings_field_subscription_purchase_description_en_description": "Английская версия текста на этапе оплаты.",
|
||||
"admin_settings_field_stars_enabled_label": "Telegram Stars",
|
||||
"admin_settings_field_yookassa_enabled_label": "Включена",
|
||||
"admin_settings_field_yookassa_shop_id_label": "Shop ID",
|
||||
|
||||
@@ -16,6 +16,12 @@ SUPPORT_RELATED_SETTINGS = (
|
||||
"SUPPORT_TICKET_RATE_LIMIT_PER_HOUR",
|
||||
)
|
||||
|
||||
SUBSCRIPTION_PURCHASE_DESCRIPTION_SETTINGS = (
|
||||
"SUBSCRIPTION_PURCHASE_DESCRIPTION_ENABLED",
|
||||
"SUBSCRIPTION_PURCHASE_DESCRIPTION_RU",
|
||||
"SUBSCRIPTION_PURCHASE_DESCRIPTION_EN",
|
||||
)
|
||||
|
||||
|
||||
def _manifest_by_key() -> dict[str, dict]:
|
||||
return {item["key"]: item for item in manifest_payload()}
|
||||
@@ -50,3 +56,15 @@ def test_support_settings_i18n_keys_exist_in_admin_locales():
|
||||
field = manifest[setting_key]
|
||||
assert field["i18n_label_key"] in messages
|
||||
assert field["i18n_description_key"] in messages
|
||||
|
||||
|
||||
def test_subscription_purchase_description_settings_i18n_keys_exist():
|
||||
manifest = _manifest_by_key()
|
||||
|
||||
for language in ("ru", "en"):
|
||||
messages = _locale(language)
|
||||
for setting_key in SUBSCRIPTION_PURCHASE_DESCRIPTION_SETTINGS:
|
||||
field = manifest[setting_key]
|
||||
assert field["section"] == "pricing"
|
||||
assert field["i18n_label_key"] in messages
|
||||
assert field["i18n_description_key"] in messages
|
||||
|
||||
@@ -51,8 +51,13 @@ class ConfigureLoggingTests(unittest.TestCase):
|
||||
# Confirm the format string structure by emitting a record into a buffer.
|
||||
formatter = handler.formatter
|
||||
record = logging.LogRecord(
|
||||
name="test", level=logging.INFO, pathname=__file__, lineno=1,
|
||||
msg="hello", args=(), exc_info=None,
|
||||
name="test",
|
||||
level=logging.INFO,
|
||||
pathname=__file__,
|
||||
lineno=1,
|
||||
msg="hello",
|
||||
args=(),
|
||||
exc_info=None,
|
||||
)
|
||||
rendered = formatter.format(record) if formatter else ""
|
||||
# Format is "%(asctime)s - %(name)s - %(levelname)s - %(message)s".
|
||||
|
||||
@@ -155,15 +155,11 @@ class LiveGitFallbackTests(unittest.TestCase):
|
||||
return _resolve()
|
||||
|
||||
def test_tag_with_zero_commits_since_returns_bare_tag(self):
|
||||
result = self._run_with_git(
|
||||
{"tag": "v2.0.0", "sha": "abcdef1", "commits_since_tag": "0"}
|
||||
)
|
||||
result = self._run_with_git({"tag": "v2.0.0", "sha": "abcdef1", "commits_since_tag": "0"})
|
||||
self.assertEqual(result, "v2.0.0")
|
||||
|
||||
def test_tag_plus_distance_plus_sha_format(self):
|
||||
result = self._run_with_git(
|
||||
{"tag": "v2.0.0", "sha": "abcdef1", "commits_since_tag": "7"}
|
||||
)
|
||||
result = self._run_with_git({"tag": "v2.0.0", "sha": "abcdef1", "commits_since_tag": "7"})
|
||||
self.assertEqual(result, "v2.0.0+7.gabcdef1")
|
||||
|
||||
def test_sha_only_when_no_tag(self):
|
||||
|
||||
@@ -33,9 +33,7 @@ class _FakeYooKassaService:
|
||||
def __init__(self, configured: bool = True, response: Optional[Dict[str, Any]] = None) -> None:
|
||||
self.configured = configured
|
||||
self.calls: List[Dict[str, Any]] = []
|
||||
self._response = response if response is not None else {
|
||||
"id": "pay-1", "status": "pending"
|
||||
}
|
||||
self._response = response if response is not None else {"id": "pay-1", "status": "pending"}
|
||||
|
||||
async def create_payment(self, **kwargs):
|
||||
self.calls.append(kwargs)
|
||||
|
||||
@@ -29,16 +29,22 @@ from config.settings import Settings
|
||||
# Strip all provider env so per-provider BaseSettings models don't pick up
|
||||
# real credentials from the local .env file during tests.
|
||||
_PROVIDER_ENV_PREFIXES = (
|
||||
"FREEKASSA_", "PLATEGA_", "SEVERPAY_", "WATA_", "HELEKET_",
|
||||
"CRYPTOPAY_", "YOOKASSA_", "STARS_",
|
||||
"FREEKASSA_",
|
||||
"PLATEGA_",
|
||||
"SEVERPAY_",
|
||||
"WATA_",
|
||||
"HELEKET_",
|
||||
"CRYPTOPAY_",
|
||||
"YOOKASSA_",
|
||||
"STARS_",
|
||||
)
|
||||
|
||||
|
||||
def _clean_env() -> dict[str, str]:
|
||||
return {
|
||||
k: v for k, v in os.environ.items()
|
||||
if not any(k.startswith(p) for p in _PROVIDER_ENV_PREFIXES)
|
||||
and not k.startswith("PAYMENT_")
|
||||
k: v
|
||||
for k, v in os.environ.items()
|
||||
if not any(k.startswith(p) for p in _PROVIDER_ENV_PREFIXES) and not k.startswith("PAYMENT_")
|
||||
}
|
||||
|
||||
|
||||
@@ -116,9 +122,7 @@ class BuildServicesWiringTests(unittest.TestCase):
|
||||
panel_webhook = services["panel_webhook_service"]
|
||||
subscription = services["subscription_service"]
|
||||
self.assertIsInstance(panel_webhook, PanelWebhookService)
|
||||
self.assertIs(
|
||||
getattr(panel_webhook, "subscription_service", None), subscription
|
||||
)
|
||||
self.assertIs(getattr(panel_webhook, "subscription_service", None), subscription)
|
||||
|
||||
def test_factory_returns_every_documented_service(self):
|
||||
"""Guards against silently dropping a service from the bundle. The
|
||||
|
||||
@@ -59,18 +59,14 @@ class MigrationDocumentationFactsTests(unittest.TestCase):
|
||||
def test_doc_lists_every_running_container_in_current_compose(self):
|
||||
"""The architecture table must reflect what ``docker compose up``
|
||||
actually produces today."""
|
||||
missing = sorted(
|
||||
name for name in EXPECTED_CONTAINER_NAMES if name not in self.doc
|
||||
)
|
||||
missing = sorted(name for name in EXPECTED_CONTAINER_NAMES if name not in self.doc)
|
||||
self.assertFalse(
|
||||
missing,
|
||||
f"migration-to-minishop.md is missing container names from current compose: {missing}",
|
||||
)
|
||||
|
||||
def test_doc_lists_every_volume_in_current_compose(self):
|
||||
missing = sorted(
|
||||
name for name in EXPECTED_VOLUME_NAMES if name not in self.doc
|
||||
)
|
||||
missing = sorted(name for name in EXPECTED_VOLUME_NAMES if name not in self.doc)
|
||||
self.assertFalse(
|
||||
missing,
|
||||
f"migration-to-minishop.md is missing volume names from current compose: {missing}",
|
||||
@@ -185,8 +181,7 @@ class MigrationScriptCoverageTests(unittest.TestCase):
|
||||
missing = sorted(EXPECTED_CONTAINER_NAMES - self.known)
|
||||
self.assertFalse(
|
||||
missing,
|
||||
f"KNOWN_CONTAINERS missing split-arch entries: {missing}\n"
|
||||
f"actual: {sorted(self.known)}",
|
||||
f"KNOWN_CONTAINERS missing split-arch entries: {missing}\nactual: {sorted(self.known)}",
|
||||
)
|
||||
|
||||
def test_known_containers_still_covers_legacy_eras(self):
|
||||
|
||||
@@ -86,9 +86,7 @@ class HandleWebhookQueueingTests(unittest.IsolatedAsyncioTestCase):
|
||||
captured: List[dict] = []
|
||||
|
||||
async def fake_enqueue(settings, provider, payload, *, event_id=None):
|
||||
captured.append(
|
||||
{"provider": provider, "payload": payload, "event_id": event_id}
|
||||
)
|
||||
captured.append({"provider": provider, "payload": payload, "event_id": event_id})
|
||||
return True
|
||||
|
||||
body = json.dumps(
|
||||
@@ -121,9 +119,7 @@ class HandleWebhookQueueingTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def fake_handle_event(event_name, user_payload):
|
||||
background_seen.append((event_name, user_payload))
|
||||
|
||||
body = json.dumps(
|
||||
{"name": "user.expired", "payload": {"telegramId": 7}}
|
||||
).encode()
|
||||
body = json.dumps({"name": "user.expired", "payload": {"telegramId": 7}}).encode()
|
||||
|
||||
with (
|
||||
patch.object(pws, "enqueue_webhook_event", fake_enqueue),
|
||||
|
||||
@@ -97,11 +97,7 @@ def test_yookassa_provider_keeps_autorenew_entrypoints_local():
|
||||
|
||||
|
||||
def test_every_payment_method_has_registry_driven_webapp_creator():
|
||||
missing = [
|
||||
spec.id
|
||||
for spec in iter_provider_specs()
|
||||
if spec.create_webapp_payment is None
|
||||
]
|
||||
missing = [spec.id for spec in iter_provider_specs() if spec.create_webapp_payment is None]
|
||||
|
||||
assert missing == []
|
||||
|
||||
@@ -192,10 +188,7 @@ def test_provider_presentation_ignores_cross_language_override():
|
||||
|
||||
settings = SimpleNamespace(PAYMENT_YOOKASSA_WEBAPP_LABEL_RU="Карта")
|
||||
|
||||
assert (
|
||||
resolve_provider_presentation(spec, settings, language="en").webapp_label
|
||||
== "Bank card"
|
||||
)
|
||||
assert resolve_provider_presentation(spec, settings, language="en").webapp_label == "Bank card"
|
||||
|
||||
|
||||
def test_payment_method_keyboard_uses_custom_telegram_text_without_changing_callback(monkeypatch):
|
||||
@@ -255,12 +248,15 @@ def test_provider_callbacks_are_built_from_specs():
|
||||
)
|
||||
== "pay_stars:1:42:subscription"
|
||||
)
|
||||
assert stars.callback_data(
|
||||
value="1",
|
||||
rub_price=150,
|
||||
stars_price=None,
|
||||
sale_mode="subscription",
|
||||
) is None
|
||||
assert (
|
||||
stars.callback_data(
|
||||
value="1",
|
||||
rub_price=150,
|
||||
stars_price=None,
|
||||
sale_mode="subscription",
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
def test_provider_visibility_uses_service_configuration():
|
||||
|
||||
@@ -87,9 +87,7 @@ class SendPaymentSuccessEmailTests(unittest.IsolatedAsyncioTestCase):
|
||||
DEFAULT_CURRENCY_SYMBOL="RUB",
|
||||
SUBSCRIPTION_MINI_APP_URL="https://app.example.com/",
|
||||
)
|
||||
user = _FakeUser(
|
||||
user_id=42, email="buyer@example.com", language_code="en"
|
||||
)
|
||||
user = _FakeUser(user_id=42, email="buyer@example.com", language_code="en")
|
||||
|
||||
with (
|
||||
patch.object(payments_module, "render_payment_success", fake_render),
|
||||
|
||||
@@ -61,9 +61,7 @@ class SkipPathTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_no_inviter_returns_empty_payload(self):
|
||||
settings = _make_settings()
|
||||
subscription_service = AsyncMock()
|
||||
service, _bot = _make_service(
|
||||
settings=settings, subscription_service=subscription_service
|
||||
)
|
||||
service, _bot = _make_service(settings=settings, subscription_service=subscription_service)
|
||||
# Referred_by_id is None → bail out immediately.
|
||||
with patch(
|
||||
"bot.services.referral_service.user_dal.get_user_by_id",
|
||||
@@ -85,16 +83,14 @@ class SkipPathTests(unittest.IsolatedAsyncioTestCase):
|
||||
# when the same referee buys multiple subscriptions.
|
||||
settings = _make_settings(REFERRAL_ONE_BONUS_PER_REFEREE=True)
|
||||
subscription_service = AsyncMock()
|
||||
service, _bot = _make_service(
|
||||
settings=settings, subscription_service=subscription_service
|
||||
)
|
||||
service, _bot = _make_service(settings=settings, subscription_service=subscription_service)
|
||||
with (
|
||||
patch(
|
||||
"bot.services.referral_service.user_dal.get_user_by_id",
|
||||
AsyncMock(
|
||||
side_effect=lambda session, uid: _make_user(uid, referred_by_id=1)
|
||||
if uid == 42
|
||||
else _make_user(uid)
|
||||
side_effect=lambda session, uid: (
|
||||
_make_user(uid, referred_by_id=1) if uid == 42 else _make_user(uid)
|
||||
)
|
||||
),
|
||||
),
|
||||
patch(
|
||||
@@ -118,15 +114,13 @@ class SkipPathTests(unittest.IsolatedAsyncioTestCase):
|
||||
settings = _make_settings(REFERRAL_ONE_BONUS_PER_REFEREE=False)
|
||||
subscription_service = AsyncMock()
|
||||
subscription_service.has_active_subscription = AsyncMock(return_value=True)
|
||||
service, _bot = _make_service(
|
||||
settings=settings, subscription_service=subscription_service
|
||||
)
|
||||
service, _bot = _make_service(settings=settings, subscription_service=subscription_service)
|
||||
with patch(
|
||||
"bot.services.referral_service.user_dal.get_user_by_id",
|
||||
AsyncMock(
|
||||
side_effect=lambda session, uid: _make_user(uid, referred_by_id=1)
|
||||
if uid == 42
|
||||
else _make_user(uid)
|
||||
side_effect=lambda session, uid: (
|
||||
_make_user(uid, referred_by_id=1) if uid == 42 else _make_user(uid)
|
||||
)
|
||||
),
|
||||
):
|
||||
result = await service.apply_referral_bonuses_for_payment(
|
||||
@@ -156,16 +150,14 @@ class InviterBonusTests(unittest.IsolatedAsyncioTestCase):
|
||||
)
|
||||
new_end = datetime(2026, 1, 1, tzinfo=timezone.utc)
|
||||
subscription_service.extend_active_subscription_days = AsyncMock(return_value=new_end)
|
||||
service, bot = _make_service(
|
||||
settings=settings, subscription_service=subscription_service
|
||||
)
|
||||
service, bot = _make_service(settings=settings, subscription_service=subscription_service)
|
||||
|
||||
with patch(
|
||||
"bot.services.referral_service.user_dal.get_user_by_id",
|
||||
AsyncMock(
|
||||
side_effect=lambda session, uid: _make_user(uid, referred_by_id=1)
|
||||
if uid == 42
|
||||
else _make_user(uid)
|
||||
side_effect=lambda session, uid: (
|
||||
_make_user(uid, referred_by_id=1) if uid == 42 else _make_user(uid)
|
||||
)
|
||||
),
|
||||
):
|
||||
result = await service.apply_referral_bonuses_for_payment(
|
||||
@@ -204,17 +196,15 @@ class InviterBonusTests(unittest.IsolatedAsyncioTestCase):
|
||||
return_value={"ok": True}
|
||||
)
|
||||
|
||||
service, bot = _make_service(
|
||||
settings=settings, subscription_service=subscription_service
|
||||
)
|
||||
service, bot = _make_service(settings=settings, subscription_service=subscription_service)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"bot.services.referral_service.user_dal.get_user_by_id",
|
||||
AsyncMock(
|
||||
side_effect=lambda session, uid: _make_user(uid, referred_by_id=1)
|
||||
if uid == 42
|
||||
else _make_user(uid)
|
||||
side_effect=lambda session, uid: (
|
||||
_make_user(uid, referred_by_id=1) if uid == 42 else _make_user(uid)
|
||||
)
|
||||
),
|
||||
),
|
||||
patch(
|
||||
@@ -262,16 +252,14 @@ class RefereeBonusTests(unittest.IsolatedAsyncioTestCase):
|
||||
subscription_service.extend_active_subscription_days = AsyncMock(
|
||||
return_value=referee_new_end
|
||||
)
|
||||
service, _bot = _make_service(
|
||||
settings=settings, subscription_service=subscription_service
|
||||
)
|
||||
service, _bot = _make_service(settings=settings, subscription_service=subscription_service)
|
||||
|
||||
with patch(
|
||||
"bot.services.referral_service.user_dal.get_user_by_id",
|
||||
AsyncMock(
|
||||
side_effect=lambda session, uid: _make_user(uid, referred_by_id=1)
|
||||
if uid == 42
|
||||
else _make_user(uid)
|
||||
side_effect=lambda session, uid: (
|
||||
_make_user(uid, referred_by_id=1) if uid == 42 else _make_user(uid)
|
||||
)
|
||||
),
|
||||
):
|
||||
# 3-month plan → 10-day referee bonus, 21-day inviter bonus.
|
||||
@@ -303,16 +291,14 @@ class RefereeBonusTests(unittest.IsolatedAsyncioTestCase):
|
||||
subscription_service = AsyncMock()
|
||||
subscription_service.has_active_subscription = AsyncMock(return_value=False)
|
||||
subscription_service.extend_active_subscription_days = AsyncMock(return_value=None)
|
||||
service, _bot = _make_service(
|
||||
settings=settings, subscription_service=subscription_service
|
||||
)
|
||||
service, _bot = _make_service(settings=settings, subscription_service=subscription_service)
|
||||
|
||||
with patch(
|
||||
"bot.services.referral_service.user_dal.get_user_by_id",
|
||||
AsyncMock(
|
||||
side_effect=lambda session, uid: _make_user(uid, referred_by_id=1)
|
||||
if uid == 42
|
||||
else _make_user(uid)
|
||||
side_effect=lambda session, uid: (
|
||||
_make_user(uid, referred_by_id=1) if uid == 42 else _make_user(uid)
|
||||
)
|
||||
),
|
||||
):
|
||||
result = await service.apply_referral_bonuses_for_payment(
|
||||
@@ -331,16 +317,14 @@ class RefereeBonusTests(unittest.IsolatedAsyncioTestCase):
|
||||
subscription_service = AsyncMock()
|
||||
subscription_service.has_active_subscription = AsyncMock(return_value=False)
|
||||
subscription_service.extend_active_subscription_days = AsyncMock()
|
||||
service, _bot = _make_service(
|
||||
settings=settings, subscription_service=subscription_service
|
||||
)
|
||||
service, _bot = _make_service(settings=settings, subscription_service=subscription_service)
|
||||
|
||||
with patch(
|
||||
"bot.services.referral_service.user_dal.get_user_by_id",
|
||||
AsyncMock(
|
||||
side_effect=lambda session, uid: _make_user(uid, referred_by_id=1)
|
||||
if uid == 42
|
||||
else _make_user(uid)
|
||||
side_effect=lambda session, uid: (
|
||||
_make_user(uid, referred_by_id=1) if uid == 42 else _make_user(uid)
|
||||
)
|
||||
),
|
||||
):
|
||||
result = await service.apply_referral_bonuses_for_payment(
|
||||
@@ -359,9 +343,7 @@ class GenerateReferralLinkTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_includes_bot_username_and_referral_code(self):
|
||||
settings = _make_settings()
|
||||
subscription_service = AsyncMock()
|
||||
service, _bot = _make_service(
|
||||
settings=settings, subscription_service=subscription_service
|
||||
)
|
||||
service, _bot = _make_service(settings=settings, subscription_service=subscription_service)
|
||||
|
||||
with (
|
||||
patch(
|
||||
@@ -383,9 +365,7 @@ class GenerateReferralLinkTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_returns_none_when_user_missing(self):
|
||||
settings = _make_settings()
|
||||
subscription_service = AsyncMock()
|
||||
service, _bot = _make_service(
|
||||
settings=settings, subscription_service=subscription_service
|
||||
)
|
||||
service, _bot = _make_service(settings=settings, subscription_service=subscription_service)
|
||||
|
||||
with patch(
|
||||
"bot.services.referral_service.user_dal.get_user_by_id",
|
||||
@@ -401,9 +381,7 @@ class GenerateReferralLinkTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_returns_none_when_referral_code_unavailable(self):
|
||||
settings = _make_settings()
|
||||
subscription_service = AsyncMock()
|
||||
service, _bot = _make_service(
|
||||
settings=settings, subscription_service=subscription_service
|
||||
)
|
||||
service, _bot = _make_service(settings=settings, subscription_service=subscription_service)
|
||||
|
||||
with (
|
||||
patch(
|
||||
|
||||
@@ -162,6 +162,22 @@ class SettingsTests(unittest.TestCase):
|
||||
|
||||
self.assertFalse(settings.SUPPORT_ADMIN_EMAIL_NOTIFICATIONS_ENABLED)
|
||||
|
||||
def test_subscription_purchase_description_is_localized_and_toggleable(self):
|
||||
settings = Settings(
|
||||
_env_file=None,
|
||||
BOT_TOKEN="token",
|
||||
POSTGRES_USER="app_user",
|
||||
POSTGRES_PASSWORD="app_password",
|
||||
SUBSCRIPTION_PURCHASE_DESCRIPTION_RU="Русский текст",
|
||||
SUBSCRIPTION_PURCHASE_DESCRIPTION_EN="English text",
|
||||
)
|
||||
|
||||
self.assertEqual(settings.subscription_purchase_description("ru"), "Русский текст")
|
||||
self.assertEqual(settings.subscription_purchase_description("en"), "English text")
|
||||
|
||||
settings.SUBSCRIPTION_PURCHASE_DESCRIPTION_ENABLED = False
|
||||
self.assertEqual(settings.subscription_purchase_description("ru"), "")
|
||||
|
||||
def test_payment_button_presentation_env_values_are_available(self):
|
||||
"""Presentation overrides now live on each provider's BaseSettings
|
||||
model instead of the central Settings — verify they're loaded from
|
||||
|
||||
@@ -5,6 +5,7 @@ from types import SimpleNamespace
|
||||
|
||||
from bot.app.web import subscription_webapp
|
||||
from bot.handlers.user import referral
|
||||
from bot.handlers.user.subscription.core import _with_subscription_purchase_description
|
||||
from bot.keyboards.inline.user_keyboards import (
|
||||
get_bot_interface_inline_keyboard,
|
||||
get_information_links_keyboard,
|
||||
@@ -101,6 +102,30 @@ class UserBotMenuTests(unittest.TestCase):
|
||||
self.assertIn("set_lang_ru:bot", self._callback_data(language_markup))
|
||||
self.assertIn("subscribe_period:1:bot", self._callback_data(subscription_markup))
|
||||
|
||||
def test_subscription_purchase_description_is_prepended_before_period_selection(self):
|
||||
settings = SimpleNamespace(
|
||||
subscription_purchase_description=lambda language: f"Description {language}"
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
_with_subscription_purchase_description(
|
||||
"Choose period",
|
||||
settings,
|
||||
"en",
|
||||
include=True,
|
||||
),
|
||||
"Description en\n\nChoose period",
|
||||
)
|
||||
self.assertEqual(
|
||||
_with_subscription_purchase_description(
|
||||
"Choose traffic",
|
||||
settings,
|
||||
"en",
|
||||
include=False,
|
||||
),
|
||||
"Choose traffic",
|
||||
)
|
||||
|
||||
def test_payment_navigation_context_keeps_bot_menu_source(self):
|
||||
settings = SimpleNamespace(
|
||||
payment_methods_order=[],
|
||||
|
||||
@@ -58,9 +58,7 @@ class FakeRedis:
|
||||
bucket.insert(0, value)
|
||||
return len(bucket)
|
||||
|
||||
async def brpop(
|
||||
self, key: str, timeout: int = 0
|
||||
) -> Optional[Tuple[str, str]]:
|
||||
async def brpop(self, key: str, timeout: int = 0) -> Optional[Tuple[str, str]]:
|
||||
bucket = self._lists.get(key)
|
||||
if bucket:
|
||||
return key, bucket.pop()
|
||||
@@ -230,9 +228,7 @@ class RedisLockTests(unittest.IsolatedAsyncioTestCase):
|
||||
settings = _make_settings()
|
||||
async with redis_infra.redis_lock(settings, "panel-sync", ttl_seconds=30) as first:
|
||||
self.assertTrue(first)
|
||||
async with redis_infra.redis_lock(
|
||||
settings, "panel-sync", ttl_seconds=30
|
||||
) as second:
|
||||
async with redis_infra.redis_lock(settings, "panel-sync", ttl_seconds=30) as second:
|
||||
self.assertFalse(second)
|
||||
# After exit, the lock is released and can be re-acquired.
|
||||
async with redis_infra.redis_lock(settings, "panel-sync", ttl_seconds=30) as again:
|
||||
|
||||
Reference in New Issue
Block a user