fix: configure referral bonuses per tariff period
This commit is contained in:
@@ -230,6 +230,70 @@ SETTINGS_MANIFEST: List[SettingField] = [
|
||||
SettingField("STARS_PRICE_3_MONTHS", "int", "pricing", "Цена 3 мес. (Stars)"),
|
||||
SettingField("STARS_PRICE_6_MONTHS", "int", "pricing", "Цена 6 мес. (Stars)"),
|
||||
SettingField("STARS_PRICE_12_MONTHS", "int", "pricing", "Цена 12 мес. (Stars)"),
|
||||
SettingField(
|
||||
"REFERRAL_BONUS_DAYS_INVITER_1_MONTH",
|
||||
"int",
|
||||
"pricing",
|
||||
"Бонус приглашающему: 1 мес.",
|
||||
min=0,
|
||||
subsection="legacy_tariffs",
|
||||
),
|
||||
SettingField(
|
||||
"REFERRAL_BONUS_DAYS_INVITER_3_MONTHS",
|
||||
"int",
|
||||
"pricing",
|
||||
"Бонус приглашающему: 3 мес.",
|
||||
min=0,
|
||||
subsection="legacy_tariffs",
|
||||
),
|
||||
SettingField(
|
||||
"REFERRAL_BONUS_DAYS_INVITER_6_MONTHS",
|
||||
"int",
|
||||
"pricing",
|
||||
"Бонус приглашающему: 6 мес.",
|
||||
min=0,
|
||||
subsection="legacy_tariffs",
|
||||
),
|
||||
SettingField(
|
||||
"REFERRAL_BONUS_DAYS_INVITER_12_MONTHS",
|
||||
"int",
|
||||
"pricing",
|
||||
"Бонус приглашающему: 12 мес.",
|
||||
min=0,
|
||||
subsection="legacy_tariffs",
|
||||
),
|
||||
SettingField(
|
||||
"REFERRAL_BONUS_DAYS_REFEREE_1_MONTH",
|
||||
"int",
|
||||
"pricing",
|
||||
"Бонус приглашённому: 1 мес.",
|
||||
min=0,
|
||||
subsection="legacy_tariffs",
|
||||
),
|
||||
SettingField(
|
||||
"REFERRAL_BONUS_DAYS_REFEREE_3_MONTHS",
|
||||
"int",
|
||||
"pricing",
|
||||
"Бонус приглашённому: 3 мес.",
|
||||
min=0,
|
||||
subsection="legacy_tariffs",
|
||||
),
|
||||
SettingField(
|
||||
"REFERRAL_BONUS_DAYS_REFEREE_6_MONTHS",
|
||||
"int",
|
||||
"pricing",
|
||||
"Бонус приглашённому: 6 мес.",
|
||||
min=0,
|
||||
subsection="legacy_tariffs",
|
||||
),
|
||||
SettingField(
|
||||
"REFERRAL_BONUS_DAYS_REFEREE_12_MONTHS",
|
||||
"int",
|
||||
"pricing",
|
||||
"Бонус приглашённому: 12 мес.",
|
||||
min=0,
|
||||
subsection="legacy_tariffs",
|
||||
),
|
||||
SettingField(
|
||||
"TRAFFIC_PACKAGES", "string", "pricing", "Пакеты трафика", "Формат: 10:199,50:799 (ГБ:цена)"
|
||||
),
|
||||
@@ -333,62 +397,6 @@ SETTINGS_MANIFEST: List[SettingField] = [
|
||||
"REFERRAL_WELCOME_BONUS_DAYS", "int", "referral", "Приветственный бонус (дней)", min=0
|
||||
),
|
||||
SettingField("LEGACY_REFS", "bool", "referral", "Поддержка старых ref-ссылок"),
|
||||
SettingField(
|
||||
"REFERRAL_BONUS_DAYS_INVITER_1_MONTH",
|
||||
"int",
|
||||
"referral",
|
||||
"Бонус приглашающему: 1 мес.",
|
||||
min=0,
|
||||
),
|
||||
SettingField(
|
||||
"REFERRAL_BONUS_DAYS_INVITER_3_MONTHS",
|
||||
"int",
|
||||
"referral",
|
||||
"Бонус приглашающему: 3 мес.",
|
||||
min=0,
|
||||
),
|
||||
SettingField(
|
||||
"REFERRAL_BONUS_DAYS_INVITER_6_MONTHS",
|
||||
"int",
|
||||
"referral",
|
||||
"Бонус приглашающему: 6 мес.",
|
||||
min=0,
|
||||
),
|
||||
SettingField(
|
||||
"REFERRAL_BONUS_DAYS_INVITER_12_MONTHS",
|
||||
"int",
|
||||
"referral",
|
||||
"Бонус приглашающему: 12 мес.",
|
||||
min=0,
|
||||
),
|
||||
SettingField(
|
||||
"REFERRAL_BONUS_DAYS_REFEREE_1_MONTH",
|
||||
"int",
|
||||
"referral",
|
||||
"Бонус приглашённому: 1 мес.",
|
||||
min=0,
|
||||
),
|
||||
SettingField(
|
||||
"REFERRAL_BONUS_DAYS_REFEREE_3_MONTHS",
|
||||
"int",
|
||||
"referral",
|
||||
"Бонус приглашённому: 3 мес.",
|
||||
min=0,
|
||||
),
|
||||
SettingField(
|
||||
"REFERRAL_BONUS_DAYS_REFEREE_6_MONTHS",
|
||||
"int",
|
||||
"referral",
|
||||
"Бонус приглашённому: 6 мес.",
|
||||
min=0,
|
||||
),
|
||||
SettingField(
|
||||
"REFERRAL_BONUS_DAYS_REFEREE_12_MONTHS",
|
||||
"int",
|
||||
"referral",
|
||||
"Бонус приглашённому: 12 мес.",
|
||||
min=0,
|
||||
),
|
||||
# ─── Notifications ─────────────────────────────────────────────
|
||||
SettingField(
|
||||
"SUBSCRIPTION_NOTIFICATIONS_ENABLED",
|
||||
|
||||
@@ -156,12 +156,47 @@ async def _build_user_payload(request: web.Request, user_id: int) -> Dict[str, A
|
||||
}
|
||||
|
||||
|
||||
def _serialize_referral_bonus_details(settings: Settings, lang: str) -> List[Dict[str, Any]]:
|
||||
def _legacy_referral_bonus_periods(settings: Settings) -> List[int]:
|
||||
if getattr(settings, "traffic_sale_mode", False):
|
||||
return []
|
||||
|
||||
return sorted(int(months) for months in settings.subscription_options)
|
||||
|
||||
|
||||
def _serialize_tariff_referral_bonus_details(settings: Settings, lang: str) -> List[Dict[str, Any]]:
|
||||
tariffs_config = settings.tariffs_config
|
||||
if not tariffs_config:
|
||||
return []
|
||||
|
||||
details: List[Dict[str, Any]] = []
|
||||
for months, _price in sorted(settings.subscription_options.items()):
|
||||
for tariff in tariffs_config.enabled_tariffs:
|
||||
if tariff.billing_model != "period":
|
||||
continue
|
||||
for months in sorted(int(month) for month in tariff.enabled_periods):
|
||||
inviter_days = tariff.referral_inviter_bonus_days(months)
|
||||
friend_days = tariff.referral_referee_bonus_days(months)
|
||||
if inviter_days is None and friend_days is None:
|
||||
continue
|
||||
details.append(
|
||||
{
|
||||
"id": f"{tariff.key}:{months}",
|
||||
"tariff_key": tariff.key,
|
||||
"tariff_name": tariff.name(lang),
|
||||
"months": int(months),
|
||||
"title": f"{tariff.name(lang)} - {_format_months_title(int(months), lang)}",
|
||||
"inviter_days": int(inviter_days or 0),
|
||||
"friend_days": int(friend_days or 0),
|
||||
}
|
||||
)
|
||||
return details
|
||||
|
||||
|
||||
def _serialize_referral_bonus_details(settings: Settings, lang: str) -> List[Dict[str, Any]]:
|
||||
if settings.tariffs_config:
|
||||
return _serialize_tariff_referral_bonus_details(settings, lang)
|
||||
|
||||
details: List[Dict[str, Any]] = []
|
||||
for months in _legacy_referral_bonus_periods(settings):
|
||||
inviter_days = settings.referral_bonus_inviter.get(months)
|
||||
friend_days = settings.referral_bonus_referee.get(months)
|
||||
if inviter_days is None and friend_days is None:
|
||||
|
||||
@@ -16,7 +16,13 @@ from bot.utils.text_sanitizer import sanitize_display_name, username_for_display
|
||||
from db.dal import payment_dal, user_dal
|
||||
from db.models import Payment, User
|
||||
|
||||
from .common import Translator, format_human_units, make_translator, sale_mode_base
|
||||
from .common import (
|
||||
Translator,
|
||||
format_human_units,
|
||||
make_translator,
|
||||
sale_mode_base,
|
||||
sale_mode_tariff_key,
|
||||
)
|
||||
|
||||
_TRAFFIC_MODES = {"traffic", "traffic_package", "topup", "premium_topup"}
|
||||
_HWID_DEVICE_MODES = {"hwid_device", "hwid_devices", "hwid_devices_renewal"}
|
||||
@@ -304,6 +310,7 @@ async def finalize_successful_payment(
|
||||
activation_months or 1,
|
||||
current_payment_db_id=req.payment.payment_id,
|
||||
skip_if_active_before_payment=False,
|
||||
tariff_key=sale_mode_tariff_key(req.sale_mode),
|
||||
)
|
||||
await req.session.commit()
|
||||
except Exception:
|
||||
|
||||
@@ -75,6 +75,9 @@ from .shared import (
|
||||
from .shared import (
|
||||
sale_mode_base as _sale_mode_base,
|
||||
)
|
||||
from .shared import (
|
||||
sale_mode_tariff_key as _sale_mode_tariff_key,
|
||||
)
|
||||
|
||||
|
||||
class YooKassaConfig(ProviderEnvConfig):
|
||||
@@ -749,6 +752,7 @@ async def process_successful_payment(
|
||||
months_for_activation or int(subscription_months) or 1,
|
||||
current_payment_db_id=payment_db_id,
|
||||
skip_if_active_before_payment=False,
|
||||
tariff_key=_sale_mode_tariff_key(sale_mode),
|
||||
)
|
||||
applied_referee_bonus_days_from_referral: Optional[int] = None
|
||||
if referral_bonus_info and referral_bonus_info.get("referee_new_end_date"):
|
||||
|
||||
@@ -33,6 +33,7 @@ class ReferralService:
|
||||
purchased_subscription_months: int,
|
||||
current_payment_db_id: Optional[int] = None,
|
||||
skip_if_active_before_payment: bool = True,
|
||||
tariff_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
|
||||
referee_final_end_date: Optional[datetime] = None
|
||||
@@ -94,11 +95,9 @@ class ReferralService:
|
||||
else self.i18n.gettext(default_lang_for_placeholder, "friend_placeholder")
|
||||
)
|
||||
|
||||
inviter_bonus_days = self.settings.referral_bonus_inviter.get(
|
||||
purchased_subscription_months
|
||||
)
|
||||
referee_bonus_days = self.settings.referral_bonus_referee.get(
|
||||
purchased_subscription_months
|
||||
inviter_bonus_days, referee_bonus_days = self._referral_bonus_days_for_payment(
|
||||
purchased_subscription_months,
|
||||
tariff_key=tariff_key,
|
||||
)
|
||||
|
||||
if inviter_bonus_days and inviter_bonus_days > 0:
|
||||
@@ -263,6 +262,35 @@ class ReferralService:
|
||||
|
||||
raise
|
||||
|
||||
def _referral_bonus_days_for_payment(
|
||||
self,
|
||||
purchased_subscription_months: int,
|
||||
*,
|
||||
tariff_key: Optional[str] = None,
|
||||
) -> tuple[Optional[int], Optional[int]]:
|
||||
months = int(purchased_subscription_months)
|
||||
tariffs_config = getattr(self.settings, "tariffs_config", None)
|
||||
if tariff_key and tariffs_config:
|
||||
try:
|
||||
tariff = tariffs_config.require(str(tariff_key))
|
||||
except Exception:
|
||||
logging.warning(
|
||||
"Referral bonuses skipped: tariff %s was not found.",
|
||||
tariff_key,
|
||||
)
|
||||
return None, None
|
||||
if tariff.billing_model != "period":
|
||||
return None, None
|
||||
return (
|
||||
tariff.referral_inviter_bonus_days(months),
|
||||
tariff.referral_referee_bonus_days(months),
|
||||
)
|
||||
|
||||
return (
|
||||
self.settings.referral_bonus_inviter.get(months),
|
||||
self.settings.referral_bonus_referee.get(months),
|
||||
)
|
||||
|
||||
async def generate_referral_link(
|
||||
self, session: AsyncSession, bot_username: str, inviter_user_id: int
|
||||
) -> Optional[str]:
|
||||
|
||||
@@ -704,6 +704,9 @@ class SubscriptionLifecycleMixin:
|
||||
active_sub = await subscription_dal.get_active_subscription_by_user_id(
|
||||
session, user_id, panel_uuid
|
||||
)
|
||||
preserve_tariff_limits = bool(
|
||||
active_sub and active_sub.tariff_key and self._tariffs_config()
|
||||
)
|
||||
if not active_sub or not active_sub.end_date:
|
||||
logging.info(
|
||||
f"No active subscription found for user {user_id}. Creating new one for {bonus_days} days." # noqa: E501
|
||||
@@ -748,6 +751,7 @@ class SubscriptionLifecycleMixin:
|
||||
|
||||
if (
|
||||
apply_main_traffic_limit
|
||||
and not preserve_tariff_limits
|
||||
and updated_sub_model
|
||||
and updated_sub_model.traffic_limit_bytes != self.settings.user_traffic_limit_bytes
|
||||
):
|
||||
@@ -762,7 +766,9 @@ class SubscriptionLifecycleMixin:
|
||||
panel_update_payload = self._build_panel_update_payload(
|
||||
expire_at=new_end_date_obj,
|
||||
traffic_limit_bytes=(
|
||||
self.settings.user_traffic_limit_bytes if apply_main_traffic_limit else None
|
||||
self.settings.user_traffic_limit_bytes
|
||||
if apply_main_traffic_limit and not preserve_tariff_limits
|
||||
else None
|
||||
),
|
||||
include_uuid=False,
|
||||
include_default_squads=False,
|
||||
|
||||
@@ -214,10 +214,10 @@ class Settings(BaseSettings):
|
||||
MONTH_6_ENABLED: bool = Field(default=True, alias="6_MONTHS_ENABLED")
|
||||
MONTH_12_ENABLED: bool = Field(default=True, alias="12_MONTHS_ENABLED")
|
||||
|
||||
RUB_PRICE_1_MONTH: Optional[int] = Field(default=None)
|
||||
RUB_PRICE_3_MONTHS: Optional[int] = Field(default=None)
|
||||
RUB_PRICE_6_MONTHS: Optional[int] = Field(default=None)
|
||||
RUB_PRICE_12_MONTHS: Optional[int] = Field(default=None)
|
||||
RUB_PRICE_1_MONTH: Optional[int] = Field(default=200)
|
||||
RUB_PRICE_3_MONTHS: Optional[int] = Field(default=600)
|
||||
RUB_PRICE_6_MONTHS: Optional[int] = Field(default=1200)
|
||||
RUB_PRICE_12_MONTHS: Optional[int] = Field(default=2400)
|
||||
|
||||
STARS_PRICE_1_MONTH: Optional[int] = Field(default=None)
|
||||
STARS_PRICE_3_MONTHS: Optional[int] = Field(default=None)
|
||||
|
||||
@@ -95,6 +95,8 @@ class Tariff(BaseModel):
|
||||
monthly_gb: Optional[float] = None
|
||||
prices_rub: Dict[str, float] = Field(default_factory=dict)
|
||||
prices_stars: Dict[str, float] = Field(default_factory=dict)
|
||||
referral_bonus_days_inviter: Dict[str, int] = Field(default_factory=dict)
|
||||
referral_bonus_days_referee: Dict[str, int] = Field(default_factory=dict)
|
||||
enabled_periods: List[int] = Field(default_factory=list)
|
||||
topup_packages: Optional[PackageSet] = None
|
||||
|
||||
@@ -129,6 +131,14 @@ class Tariff(BaseModel):
|
||||
if self.billing_model == "period":
|
||||
if self.monthly_gb is None or self.monthly_gb < 0:
|
||||
raise ValueError(f"period tariff {self.key}: monthly_gb must be >= 0")
|
||||
self.referral_bonus_days_inviter = self._normalize_referral_bonus_map(
|
||||
self.referral_bonus_days_inviter,
|
||||
"referral_bonus_days_inviter",
|
||||
)
|
||||
self.referral_bonus_days_referee = self._normalize_referral_bonus_map(
|
||||
self.referral_bonus_days_referee,
|
||||
"referral_bonus_days_referee",
|
||||
)
|
||||
if not self.enabled_periods:
|
||||
raise ValueError(f"period tariff {self.key}: enabled_periods is required")
|
||||
for months in self.enabled_periods:
|
||||
@@ -152,6 +162,23 @@ class Tariff(BaseModel):
|
||||
)
|
||||
return self
|
||||
|
||||
def _normalize_referral_bonus_map(
|
||||
self, values: Dict[str, int], field_name: str
|
||||
) -> Dict[str, int]:
|
||||
normalized: Dict[str, int] = {}
|
||||
for period, days in (values or {}).items():
|
||||
try:
|
||||
months = int(float(str(period).strip()))
|
||||
bonus_days = int(float(days))
|
||||
except (TypeError, ValueError):
|
||||
raise ValueError(f"tariff {self.key}: {field_name} contains invalid entry")
|
||||
if months <= 0:
|
||||
raise ValueError(f"tariff {self.key}: {field_name} periods must be positive")
|
||||
if bonus_days < 0:
|
||||
raise ValueError(f"tariff {self.key}: {field_name} days must be >= 0")
|
||||
normalized[str(months)] = bonus_days
|
||||
return normalized
|
||||
|
||||
def name(self, lang: str, fallback: str = "ru") -> str:
|
||||
return self.names.get(lang) or self.names.get(fallback) or self.key
|
||||
|
||||
@@ -173,6 +200,14 @@ class Tariff(BaseModel):
|
||||
value = source.get(str(months))
|
||||
return float(value) if value is not None else None
|
||||
|
||||
def referral_inviter_bonus_days(self, months: int) -> Optional[int]:
|
||||
value = self.referral_bonus_days_inviter.get(str(int(months)))
|
||||
return int(value) if value is not None else None
|
||||
|
||||
def referral_referee_bonus_days(self, months: int) -> Optional[int]:
|
||||
value = self.referral_bonus_days_referee.get(str(int(months)))
|
||||
return int(value) if value is not None else None
|
||||
|
||||
def min_period_price_rub(self) -> Optional[float]:
|
||||
prices = [
|
||||
float(self.prices_rub[str(months)])
|
||||
|
||||
@@ -40,8 +40,10 @@
|
||||
{ "count": 1, "price": 2500 }
|
||||
]
|
||||
},
|
||||
"prices_rub": { "1": 150, "3": 400, "6": 750, "12": 1400 },
|
||||
"prices_rub": { "1": 200, "3": 600, "6": 1200, "12": 2400 },
|
||||
"prices_stars": { "1": 0, "3": 0, "6": 0, "12": 0 },
|
||||
"referral_bonus_days_inviter": { "1": 3, "3": 7, "6": 15, "12": 30 },
|
||||
"referral_bonus_days_referee": { "1": 1, "3": 3, "6": 7, "12": 15 },
|
||||
"enabled_periods": [1, 3, 6, 12],
|
||||
"enabled": true
|
||||
},
|
||||
|
||||
@@ -501,7 +501,13 @@
|
||||
<AdminButton
|
||||
size="sm"
|
||||
onclick={() =>
|
||||
tariffsStore.addDraftRow("periodRows", { months: 1, rub: "", stars: "" })}
|
||||
tariffsStore.addDraftRow("periodRows", {
|
||||
months: 1,
|
||||
rub: "",
|
||||
stars: "",
|
||||
referral_inviter: "",
|
||||
referral_referee: "",
|
||||
})}
|
||||
>
|
||||
<Plus size={13} />
|
||||
{at("tariff_btn_period", {}, "Период")}
|
||||
@@ -517,14 +523,16 @@
|
||||
</p>
|
||||
{:else}
|
||||
<div class="admin-row-editor">
|
||||
<div class="admin-row-editor-line admin-row-editor-4 admin-row-editor-header">
|
||||
<div class="admin-row-editor-line admin-row-editor-6 admin-row-editor-header">
|
||||
<span>{at("tariff_col_period_months", {}, "Срок, мес.")}</span>
|
||||
<span>{at("tariff_col_price_rub", {}, "Цена, ₽")}</span>
|
||||
<span>{at("tariff_col_price_stars_full", {}, "Цена, ⭐ Stars")}</span>
|
||||
<span>{at("tariff_col_ref_inviter", {}, "Бонус приглашающему")}</span>
|
||||
<span>{at("tariff_col_ref_referee", {}, "Бонус приглашённому")}</span>
|
||||
<span></span>
|
||||
</div>
|
||||
{#each tariffDraft.periodRows as row, index}
|
||||
<div class="admin-row-editor-line admin-row-editor-4">
|
||||
<div class="admin-row-editor-line admin-row-editor-6">
|
||||
<input
|
||||
class="input"
|
||||
type="number"
|
||||
@@ -551,6 +559,24 @@
|
||||
bind:value={row.stars}
|
||||
aria-label={at("tariff_label_price_stars", {}, "Цена в Telegram Stars")}
|
||||
/>
|
||||
<input
|
||||
class="input"
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
placeholder="3"
|
||||
bind:value={row.referral_inviter}
|
||||
aria-label={at("tariff_label_ref_inviter", {}, "Бонус приглашающему")}
|
||||
/>
|
||||
<input
|
||||
class="input"
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
placeholder="1"
|
||||
bind:value={row.referral_referee}
|
||||
aria-label={at("tariff_label_ref_referee", {}, "Бонус приглашённому")}
|
||||
/>
|
||||
<AdminButton
|
||||
size="sm"
|
||||
variant="danger"
|
||||
|
||||
@@ -36,10 +36,38 @@
|
||||
const TRIAL_RESET_KEYS = ["TRIAL_TRAFFIC_STRATEGY"];
|
||||
const TRIAL_SQUAD_KEYS = ["TRIAL_SQUAD_UUIDS"];
|
||||
const LEGACY_PERIODS = [
|
||||
["1", "MONTH_1_ENABLED", "RUB_PRICE_1_MONTH", "STARS_PRICE_1_MONTH"],
|
||||
["3", "MONTH_3_ENABLED", "RUB_PRICE_3_MONTHS", "STARS_PRICE_3_MONTHS"],
|
||||
["6", "MONTH_6_ENABLED", "RUB_PRICE_6_MONTHS", "STARS_PRICE_6_MONTHS"],
|
||||
["12", "MONTH_12_ENABLED", "RUB_PRICE_12_MONTHS", "STARS_PRICE_12_MONTHS"],
|
||||
[
|
||||
"1",
|
||||
"MONTH_1_ENABLED",
|
||||
"RUB_PRICE_1_MONTH",
|
||||
"STARS_PRICE_1_MONTH",
|
||||
"REFERRAL_BONUS_DAYS_INVITER_1_MONTH",
|
||||
"REFERRAL_BONUS_DAYS_REFEREE_1_MONTH",
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"MONTH_3_ENABLED",
|
||||
"RUB_PRICE_3_MONTHS",
|
||||
"STARS_PRICE_3_MONTHS",
|
||||
"REFERRAL_BONUS_DAYS_INVITER_3_MONTHS",
|
||||
"REFERRAL_BONUS_DAYS_REFEREE_3_MONTHS",
|
||||
],
|
||||
[
|
||||
"6",
|
||||
"MONTH_6_ENABLED",
|
||||
"RUB_PRICE_6_MONTHS",
|
||||
"STARS_PRICE_6_MONTHS",
|
||||
"REFERRAL_BONUS_DAYS_INVITER_6_MONTHS",
|
||||
"REFERRAL_BONUS_DAYS_REFEREE_6_MONTHS",
|
||||
],
|
||||
[
|
||||
"12",
|
||||
"MONTH_12_ENABLED",
|
||||
"RUB_PRICE_12_MONTHS",
|
||||
"STARS_PRICE_12_MONTHS",
|
||||
"REFERRAL_BONUS_DAYS_INVITER_12_MONTHS",
|
||||
"REFERRAL_BONUS_DAYS_REFEREE_12_MONTHS",
|
||||
],
|
||||
];
|
||||
const LEGACY_TARIFF_SETTING_KEYS = [
|
||||
...LEGACY_PERIODS.flatMap((row) => row.slice(1)),
|
||||
@@ -794,8 +822,10 @@
|
||||
<span>{at("tariffs_legacy_enabled", {}, "Enabled")}</span>
|
||||
<span>{at("payment_rub", {}, "RUB")}</span>
|
||||
<span>{at("payment_stars", {}, "Stars")}</span>
|
||||
<span>{at("tariffs_legacy_ref_inviter", {}, "Inviter")}</span>
|
||||
<span>{at("tariffs_legacy_ref_referee", {}, "Friend")}</span>
|
||||
</div>
|
||||
{#each LEGACY_PERIODS as [months, enabledKey, rubKey, starsKey]}
|
||||
{#each LEGACY_PERIODS as [months, enabledKey, rubKey, starsKey, inviterKey, refereeKey]}
|
||||
<div class="admin-legacy-tariff-row">
|
||||
<strong>{months} {at("months_short", {}, "mo")}</strong>
|
||||
<div class="admin-setting-switch">
|
||||
@@ -823,6 +853,22 @@
|
||||
value={valueForKey(starsKey, settingsDirty, settingsFieldMap)}
|
||||
oninput={(event) => setSetting(starsKey, event.currentTarget.value)}
|
||||
/>
|
||||
<input
|
||||
class="input"
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
value={valueForKey(inviterKey, settingsDirty, settingsFieldMap)}
|
||||
oninput={(event) => setSetting(inviterKey, event.currentTarget.value)}
|
||||
/>
|
||||
<input
|
||||
class="input"
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
value={valueForKey(refereeKey, settingsDirty, settingsFieldMap)}
|
||||
oninput={(event) => setSetting(refereeKey, event.currentTarget.value)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -18,10 +18,10 @@ export function emptyTariffDraft() {
|
||||
hwid_device_limit: "",
|
||||
conversion_rate_rub_per_gb: "",
|
||||
periodRows: [
|
||||
{ months: 1, rub: 150, stars: "" },
|
||||
{ months: 3, rub: 400, stars: "" },
|
||||
{ months: 6, rub: 750, stars: "" },
|
||||
{ months: 12, rub: 1400, stars: "" },
|
||||
{ months: 1, rub: 200, stars: "", referral_inviter: 3, referral_referee: 1 },
|
||||
{ months: 3, rub: 600, stars: "", referral_inviter: 7, referral_referee: 3 },
|
||||
{ months: 6, rub: 1200, stars: "", referral_inviter: 15, referral_referee: 7 },
|
||||
{ months: 12, rub: 2400, stars: "", referral_inviter: 30, referral_referee: 15 },
|
||||
],
|
||||
topupRubRows: [],
|
||||
topupStarsRows: [],
|
||||
@@ -67,6 +67,8 @@ export function draftFromTariff(tariff) {
|
||||
months: month,
|
||||
rub: tariff.prices_rub?.[String(month)] ?? "",
|
||||
stars: tariff.prices_stars?.[String(month)] ?? "",
|
||||
referral_inviter: tariff.referral_bonus_days_inviter?.[String(month)] ?? "",
|
||||
referral_referee: tariff.referral_bonus_days_referee?.[String(month)] ?? "",
|
||||
}));
|
||||
|
||||
return {
|
||||
@@ -190,6 +192,8 @@ export function tariffFromDraft(draft) {
|
||||
months: parseIntNumber(row.months),
|
||||
rub: parseNumber(row.rub, 0),
|
||||
stars: parseNumber(row.stars, 0),
|
||||
referral_inviter: parseIntNumber(row.referral_inviter),
|
||||
referral_referee: parseIntNumber(row.referral_referee),
|
||||
}))
|
||||
.filter((row) => row.months > 0)
|
||||
.filter((row) => {
|
||||
@@ -204,6 +208,16 @@ export function tariffFromDraft(draft) {
|
||||
tariff.prices_stars = Object.fromEntries(
|
||||
rows.map((row) => [String(row.months), row.stars || 0])
|
||||
);
|
||||
tariff.referral_bonus_days_inviter = Object.fromEntries(
|
||||
rows
|
||||
.filter((row) => row.referral_inviter !== null)
|
||||
.map((row) => [String(row.months), row.referral_inviter])
|
||||
);
|
||||
tariff.referral_bonus_days_referee = Object.fromEntries(
|
||||
rows
|
||||
.filter((row) => row.referral_referee !== null)
|
||||
.map((row) => [String(row.months), row.referral_referee])
|
||||
);
|
||||
const topupPackages = packageSetFromRows(draft.topupRubRows, draft.topupStarsRows, "gb");
|
||||
if (topupPackages) tariff.topup_packages = topupPackages;
|
||||
} else {
|
||||
|
||||
@@ -1689,17 +1689,25 @@ export async function mockApi(path, options = {}, context = {}) {
|
||||
},
|
||||
...[
|
||||
["MONTH_1_ENABLED", "bool", true],
|
||||
["RUB_PRICE_1_MONTH", "float", 150],
|
||||
["RUB_PRICE_1_MONTH", "float", 200],
|
||||
["STARS_PRICE_1_MONTH", "int", 0],
|
||||
["REFERRAL_BONUS_DAYS_INVITER_1_MONTH", "int", 3],
|
||||
["REFERRAL_BONUS_DAYS_REFEREE_1_MONTH", "int", 1],
|
||||
["MONTH_3_ENABLED", "bool", true],
|
||||
["RUB_PRICE_3_MONTHS", "float", 400],
|
||||
["RUB_PRICE_3_MONTHS", "float", 600],
|
||||
["STARS_PRICE_3_MONTHS", "int", 0],
|
||||
["REFERRAL_BONUS_DAYS_INVITER_3_MONTHS", "int", 7],
|
||||
["REFERRAL_BONUS_DAYS_REFEREE_3_MONTHS", "int", 3],
|
||||
["MONTH_6_ENABLED", "bool", false],
|
||||
["RUB_PRICE_6_MONTHS", "float", 750],
|
||||
["RUB_PRICE_6_MONTHS", "float", 1200],
|
||||
["STARS_PRICE_6_MONTHS", "int", 0],
|
||||
["REFERRAL_BONUS_DAYS_INVITER_6_MONTHS", "int", 15],
|
||||
["REFERRAL_BONUS_DAYS_REFEREE_6_MONTHS", "int", 7],
|
||||
["MONTH_12_ENABLED", "bool", false],
|
||||
["RUB_PRICE_12_MONTHS", "float", 1200],
|
||||
["RUB_PRICE_12_MONTHS", "float", 2400],
|
||||
["STARS_PRICE_12_MONTHS", "int", 0],
|
||||
["REFERRAL_BONUS_DAYS_INVITER_12_MONTHS", "int", 30],
|
||||
["REFERRAL_BONUS_DAYS_REFEREE_12_MONTHS", "int", 15],
|
||||
["TRAFFIC_PACKAGES", "string", "10:99,50:399"],
|
||||
["STARS_TRAFFIC_PACKAGES", "string", ""],
|
||||
].map(([key, type, value]) => ({
|
||||
|
||||
@@ -3274,7 +3274,9 @@
|
||||
|
||||
.admin-legacy-tariff-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(72px, 0.7fr) minmax(92px, 0.8fr) minmax(0, 1fr) minmax(0, 1fr);
|
||||
grid-template-columns:
|
||||
minmax(58px, 0.7fr) minmax(74px, 0.8fr) minmax(0, 1fr) minmax(0, 1fr)
|
||||
minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
@@ -3393,6 +3395,12 @@
|
||||
grid-template-columns: minmax(80px, 0.8fr) minmax(100px, 1fr) minmax(100px, 1fr) 32px;
|
||||
}
|
||||
|
||||
.admin-row-editor-line.admin-row-editor-6 {
|
||||
grid-template-columns:
|
||||
minmax(72px, 0.8fr) minmax(90px, 1fr) minmax(90px, 1fr) minmax(120px, 1fr)
|
||||
minmax(120px, 1fr) 32px;
|
||||
}
|
||||
|
||||
.admin-package-columns {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
@@ -3568,7 +3576,8 @@
|
||||
.admin-form-row.admin-form-row-3,
|
||||
.admin-package-columns,
|
||||
.admin-row-editor-line,
|
||||
.admin-row-editor-line.admin-row-editor-4 {
|
||||
.admin-row-editor-line.admin-row-editor-4,
|
||||
.admin-row-editor-line.admin-row-editor-6 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
{#if referralBonusDetails.length}
|
||||
<p class="referral-bonus-intro">{t("wa_referral_bonus_paid_intro")}</p>
|
||||
{/if}
|
||||
{#each referralBonusDetails as bonus, index (bonus.months || index)}
|
||||
{#each referralBonusDetails as bonus, index (bonus.id || `${bonus.tariff_key || "legacy"}:${bonus.months || index}`)}
|
||||
<div class="referral-bonus-row">
|
||||
<strong>{bonus.title || `${bonus.months || "?"}`}</strong>
|
||||
<small
|
||||
|
||||
@@ -1261,10 +1261,14 @@
|
||||
"admin_tariff_col_price_stars": "Price, ⭐",
|
||||
"admin_tariff_col_price_stars_full": "Price, ⭐ Stars",
|
||||
"admin_tariff_col_period_months": "Period, mo.",
|
||||
"admin_tariff_col_ref_inviter": "Inviter bonus",
|
||||
"admin_tariff_col_ref_referee": "Friend bonus",
|
||||
"admin_tariff_col_hwid_count": "+ devices",
|
||||
"admin_tariff_label_hwid_count_full": "How many devices does this package add",
|
||||
"admin_tariff_label_price_rub": "Price in Rubles",
|
||||
"admin_tariff_label_price_stars": "Price in Telegram Stars",
|
||||
"admin_tariff_label_ref_inviter": "Inviter bonus, days",
|
||||
"admin_tariff_label_ref_referee": "Friend bonus, days",
|
||||
"admin_aria_label_main_traffic": "Main traffic usage",
|
||||
"admin_aria_label_premium_traffic": "Premium traffic usage",
|
||||
"admin_btn_delete": "Delete",
|
||||
@@ -1347,6 +1351,8 @@
|
||||
"admin_tariffs_legacy_subtitle": "Old remnawave-tg-shop periods and traffic packages used only when the JSON tariff catalog is not configured.",
|
||||
"admin_tariffs_legacy_period": "Period",
|
||||
"admin_tariffs_legacy_enabled": "Enabled",
|
||||
"admin_tariffs_legacy_ref_inviter": "Inviter bonus",
|
||||
"admin_tariffs_legacy_ref_referee": "Friend bonus",
|
||||
"admin_tariffs_legacy_stars_traffic_packages": "Traffic packages, Stars",
|
||||
"admin_tariffs_legacy_traffic_hint": "Format: 10:199,50:799",
|
||||
"admin_tariff_tab_general": "General",
|
||||
|
||||
@@ -1261,10 +1261,14 @@
|
||||
"admin_tariff_col_price_stars": "Цена, ⭐",
|
||||
"admin_tariff_col_price_stars_full": "Цена, ⭐ Stars",
|
||||
"admin_tariff_col_period_months": "Срок, мес.",
|
||||
"admin_tariff_col_ref_inviter": "Бонус пригласившему",
|
||||
"admin_tariff_col_ref_referee": "Бонус приглашённому",
|
||||
"admin_tariff_col_hwid_count": "+ устройств",
|
||||
"admin_tariff_label_hwid_count_full": "Сколько устройств добавляет пакет",
|
||||
"admin_tariff_label_price_rub": "Цена в рублях",
|
||||
"admin_tariff_label_price_stars": "Цена в Telegram Stars",
|
||||
"admin_tariff_label_ref_inviter": "Бонус пригласившему, дней",
|
||||
"admin_tariff_label_ref_referee": "Бонус приглашённому, дней",
|
||||
"admin_aria_label_main_traffic": "Использование основного трафика",
|
||||
"admin_aria_label_premium_traffic": "Использование premium-трафика",
|
||||
"admin_btn_delete": "Удалить",
|
||||
@@ -1347,6 +1351,8 @@
|
||||
"admin_tariffs_legacy_subtitle": "Старые периоды и пакеты трафика remnawave-tg-shop, которые используются только без JSON-каталога.",
|
||||
"admin_tariffs_legacy_period": "Период",
|
||||
"admin_tariffs_legacy_enabled": "Включён",
|
||||
"admin_tariffs_legacy_ref_inviter": "Бонус пригласившему",
|
||||
"admin_tariffs_legacy_ref_referee": "Бонус другу",
|
||||
"admin_tariffs_legacy_stars_traffic_packages": "Пакеты трафика, Stars",
|
||||
"admin_tariffs_legacy_traffic_hint": "Формат: 10:199,50:799",
|
||||
"admin_tariff_tab_general": "Основное",
|
||||
|
||||
@@ -66,6 +66,8 @@ ADMIN_TARIFF_SETTINGS_PAGE_KEYS = {
|
||||
"admin_tariffs_legacy_subtitle",
|
||||
"admin_tariffs_legacy_period",
|
||||
"admin_tariffs_legacy_enabled",
|
||||
"admin_tariffs_legacy_ref_inviter",
|
||||
"admin_tariffs_legacy_ref_referee",
|
||||
"admin_tariffs_legacy_traffic_packages",
|
||||
"admin_tariffs_legacy_stars_traffic_packages",
|
||||
"admin_tariffs_legacy_traffic_hint",
|
||||
|
||||
@@ -23,6 +23,7 @@ from typing import Any, Optional
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from bot.services.referral_service import ReferralService
|
||||
from config.tariffs_config import TariffsConfig
|
||||
|
||||
|
||||
def _make_settings(**overrides: Any) -> SimpleNamespace:
|
||||
@@ -338,6 +339,87 @@ class RefereeBonusTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertFalse(result["inviter_bonus_applied_flag"])
|
||||
subscription_service.extend_active_subscription_days.assert_not_called()
|
||||
|
||||
async def test_tariff_bonus_uses_referee_purchase_tariff(self):
|
||||
data = {
|
||||
"default_tariff": "standard",
|
||||
"tariffs": [
|
||||
{
|
||||
"key": "standard",
|
||||
"names": {"en": "Standard"},
|
||||
"descriptions": {},
|
||||
"squad_uuids": ["standard-squad"],
|
||||
"billing_model": "period",
|
||||
"monthly_gb": 100,
|
||||
"prices_rub": {"2": 400},
|
||||
"prices_stars": {},
|
||||
"enabled_periods": [2],
|
||||
"referral_bonus_days_inviter": {"2": 5},
|
||||
"referral_bonus_days_referee": {"2": 1},
|
||||
"enabled": True,
|
||||
},
|
||||
{
|
||||
"key": "premium",
|
||||
"names": {"en": "Premium"},
|
||||
"descriptions": {},
|
||||
"squad_uuids": ["premium-squad"],
|
||||
"billing_model": "period",
|
||||
"monthly_gb": 500,
|
||||
"prices_rub": {"2": 700},
|
||||
"prices_stars": {},
|
||||
"enabled_periods": [2],
|
||||
"referral_bonus_days_inviter": {"2": 20},
|
||||
"referral_bonus_days_referee": {"2": 7},
|
||||
"enabled": True,
|
||||
},
|
||||
],
|
||||
}
|
||||
settings = _make_settings(
|
||||
REFERRAL_ONE_BONUS_PER_REFEREE=False,
|
||||
tariffs_config=TariffsConfig.model_validate(data),
|
||||
)
|
||||
subscription_service = AsyncMock()
|
||||
subscription_service.has_active_subscription = AsyncMock(return_value=False)
|
||||
subscription_service._get_or_create_panel_user_link_details = AsyncMock(
|
||||
return_value=("inviter-panel", "inviter-sub", "short", False)
|
||||
)
|
||||
inviter_new_end = datetime(2026, 4, 1, tzinfo=timezone.utc)
|
||||
referee_new_end = datetime(2026, 5, 1, tzinfo=timezone.utc)
|
||||
subscription_service.extend_active_subscription_days = AsyncMock(
|
||||
side_effect=[inviter_new_end, referee_new_end]
|
||||
)
|
||||
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)
|
||||
)
|
||||
),
|
||||
):
|
||||
result = await service.apply_referral_bonuses_for_payment(
|
||||
session=AsyncMock(),
|
||||
referee_user_id=42,
|
||||
purchased_subscription_months=2,
|
||||
skip_if_active_before_payment=False,
|
||||
tariff_key="premium",
|
||||
)
|
||||
|
||||
self.assertEqual(result["referee_bonus_applied_days"], 7)
|
||||
self.assertTrue(result["inviter_bonus_applied_flag"])
|
||||
inviter_call = [
|
||||
call
|
||||
for call in subscription_service.extend_active_subscription_days.await_args_list
|
||||
if call.kwargs.get("user_id") == 1
|
||||
][0]
|
||||
referee_call = [
|
||||
call
|
||||
for call in subscription_service.extend_active_subscription_days.await_args_list
|
||||
if call.kwargs.get("user_id") == 42
|
||||
][0]
|
||||
self.assertEqual(inviter_call.kwargs["bonus_days"], 20)
|
||||
self.assertEqual(referee_call.kwargs["bonus_days"], 7)
|
||||
|
||||
|
||||
class GenerateReferralLinkTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_includes_bot_username_and_referral_code(self):
|
||||
|
||||
@@ -42,6 +42,20 @@ class SettingsTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(settings.WEBAPP_TITLE, "/minishop")
|
||||
|
||||
def test_legacy_subscription_prices_have_defaults(self):
|
||||
settings = Settings(
|
||||
_env_file=None,
|
||||
BOT_TOKEN="token",
|
||||
POSTGRES_USER="app_user",
|
||||
POSTGRES_PASSWORD="app_password",
|
||||
TARIFFS_CONFIG_PATH="missing-tariffs.json",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
settings.subscription_options,
|
||||
{1: 200.0, 3: 600.0, 6: 1200.0, 12: 2400.0},
|
||||
)
|
||||
|
||||
def test_subscription_guides_defaults_are_enabled(self):
|
||||
settings = Settings(
|
||||
_env_file=None,
|
||||
|
||||
@@ -393,6 +393,65 @@ class SubscriptionServiceActivationDispatchTests(unittest.IsolatedAsyncioTestCas
|
||||
self.assertEqual(kwargs["payment_db_id"], 12)
|
||||
|
||||
|
||||
class SubscriptionServiceBonusExtensionTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_referral_extension_preserves_existing_tariff_limit(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
settings = _make_settings(
|
||||
_tariffs_config_payload(),
|
||||
tmpdir,
|
||||
USER_TRAFFIC_LIMIT_GB=999,
|
||||
)
|
||||
service = _make_service(settings)
|
||||
service._get_or_create_panel_user_link_details = AsyncMock(
|
||||
return_value=("panel-user", "short-uuid", "short", False)
|
||||
)
|
||||
service.panel_service.update_user_details_on_panel = AsyncMock(
|
||||
return_value={"ok": True}
|
||||
)
|
||||
active_sub = SimpleNamespace(
|
||||
subscription_id=10,
|
||||
end_date=datetime.now(timezone.utc) + timedelta(days=5),
|
||||
traffic_limit_bytes=100 * GIB,
|
||||
tariff_key="standard",
|
||||
)
|
||||
updated_sub = SimpleNamespace(
|
||||
subscription_id=10,
|
||||
end_date=active_sub.end_date + timedelta(days=3),
|
||||
traffic_limit_bytes=100 * GIB,
|
||||
tariff_key="standard",
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"bot.services.subscription_service_impl.lifecycle.user_dal.get_user_by_id",
|
||||
AsyncMock(return_value=SimpleNamespace(user_id=42)),
|
||||
),
|
||||
patch(
|
||||
"bot.services.subscription_service_impl.lifecycle.subscription_dal.get_active_subscription_by_user_id",
|
||||
AsyncMock(return_value=active_sub),
|
||||
),
|
||||
patch(
|
||||
"bot.services.subscription_service_impl.lifecycle.subscription_dal.update_subscription_end_date",
|
||||
AsyncMock(return_value=updated_sub),
|
||||
),
|
||||
patch(
|
||||
"bot.services.subscription_service_impl.lifecycle.subscription_dal.update_subscription",
|
||||
AsyncMock(),
|
||||
) as update_subscription,
|
||||
):
|
||||
await service.extend_active_subscription_days(
|
||||
session=AsyncMock(),
|
||||
user_id=42,
|
||||
bonus_days=3,
|
||||
reason="referral bonus from Alice",
|
||||
)
|
||||
|
||||
update_subscription.assert_not_awaited()
|
||||
payload = service.panel_service.update_user_details_on_panel.await_args.args[1]
|
||||
self.assertNotIn("trafficLimitBytes", payload)
|
||||
self.assertNotIn("trafficLimitStrategy", payload)
|
||||
|
||||
|
||||
class SubscriptionServiceActiveDetailsTests(unittest.IsolatedAsyncioTestCase):
|
||||
def _local_active_sub(self) -> SimpleNamespace:
|
||||
return SimpleNamespace(
|
||||
|
||||
@@ -70,6 +70,28 @@ class TariffsConfigTests(unittest.TestCase):
|
||||
self.assertIsNotNone(packages)
|
||||
self.assertEqual(packages.rub[0].gb, 25)
|
||||
|
||||
def test_period_tariff_referral_bonuses_load(self):
|
||||
data = _valid_config()
|
||||
data["tariffs"][0]["referral_bonus_days_inviter"] = {"2": 5, "4": 10}
|
||||
data["tariffs"][0]["referral_bonus_days_referee"] = {"2": 1, "4": 2}
|
||||
data["tariffs"][0]["prices_rub"] = {"2": 400, "4": 800}
|
||||
data["tariffs"][0]["prices_stars"] = {}
|
||||
data["tariffs"][0]["enabled_periods"] = [2, 4]
|
||||
|
||||
config = TariffsConfig.model_validate(data)
|
||||
tariff = config.require("standard")
|
||||
|
||||
self.assertEqual(tariff.referral_inviter_bonus_days(2), 5)
|
||||
self.assertEqual(tariff.referral_referee_bonus_days(4), 2)
|
||||
self.assertIsNone(tariff.referral_inviter_bonus_days(8))
|
||||
|
||||
def test_negative_tariff_referral_bonus_rejected(self):
|
||||
data = _valid_config()
|
||||
data["tariffs"][0]["referral_bonus_days_inviter"] = {"1": -1}
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
TariffsConfig.model_validate(data)
|
||||
|
||||
def test_missing_config_returns_none(self):
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
@@ -83,6 +83,98 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertEqual(plans[1]["traffic_gb"], 50.0)
|
||||
self.assertEqual(plans[1]["stars_price"], 2500)
|
||||
|
||||
def test_referral_bonus_details_use_custom_tariff_periods(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
path = Path(tmpdir) / "tariffs.json"
|
||||
path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"default_tariff": "standard",
|
||||
"tariffs": [
|
||||
{
|
||||
"key": "standard",
|
||||
"names": {"en": "Standard"},
|
||||
"descriptions": {"en": "Custom periods"},
|
||||
"squad_uuids": ["uuid"],
|
||||
"billing_model": "period",
|
||||
"monthly_gb": 100,
|
||||
"prices_rub": {
|
||||
"2": 400,
|
||||
"4": 800,
|
||||
"8": 1600,
|
||||
"16": 3200,
|
||||
},
|
||||
"prices_stars": {},
|
||||
"referral_bonus_days_inviter": {
|
||||
"2": 5,
|
||||
"4": 10,
|
||||
"16": 40,
|
||||
},
|
||||
"referral_bonus_days_referee": {
|
||||
"2": 1,
|
||||
"4": 2,
|
||||
"8": 4,
|
||||
},
|
||||
"enabled_periods": [2, 4, 8, 16],
|
||||
"enabled": True,
|
||||
}
|
||||
],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
settings = Settings(
|
||||
_env_file=None,
|
||||
BOT_TOKEN="token",
|
||||
POSTGRES_USER="app_user",
|
||||
POSTGRES_PASSWORD="app_password",
|
||||
TARIFFS_CONFIG_PATH=str(path),
|
||||
)
|
||||
|
||||
details = subscription_webapp._serialize_referral_bonus_details(settings, "en")
|
||||
|
||||
self.assertEqual(
|
||||
details,
|
||||
[
|
||||
{
|
||||
"id": "standard:2",
|
||||
"tariff_key": "standard",
|
||||
"tariff_name": "Standard",
|
||||
"months": 2,
|
||||
"title": "Standard - 2 months",
|
||||
"inviter_days": 5,
|
||||
"friend_days": 1,
|
||||
},
|
||||
{
|
||||
"id": "standard:4",
|
||||
"tariff_key": "standard",
|
||||
"tariff_name": "Standard",
|
||||
"months": 4,
|
||||
"title": "Standard - 4 months",
|
||||
"inviter_days": 10,
|
||||
"friend_days": 2,
|
||||
},
|
||||
{
|
||||
"id": "standard:8",
|
||||
"tariff_key": "standard",
|
||||
"tariff_name": "Standard",
|
||||
"months": 8,
|
||||
"title": "Standard - 8 months",
|
||||
"inviter_days": 0,
|
||||
"friend_days": 4,
|
||||
},
|
||||
{
|
||||
"id": "standard:16",
|
||||
"tariff_key": "standard",
|
||||
"tariff_name": "Standard",
|
||||
"months": 16,
|
||||
"title": "Standard - 16 months",
|
||||
"inviter_days": 40,
|
||||
"friend_days": 0,
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
def test_subscription_template_does_not_block_on_telegram_sdk(self):
|
||||
html = subscription_webapp.TEMPLATE_PATH.read_text(encoding="utf-8")
|
||||
|
||||
@@ -653,6 +745,9 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
||||
YOOKASSA_ENABLED=False,
|
||||
CRYPTOPAY_ENABLED=False,
|
||||
TARIFFS_CONFIG_PATH="missing-tariffs.json",
|
||||
MONTH_3_ENABLED=False,
|
||||
MONTH_6_ENABLED=False,
|
||||
MONTH_12_ENABLED=False,
|
||||
RUB_PRICE_1_MONTH=None,
|
||||
STARS_PRICE_1_MONTH=250,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user