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:
|
||||
@@ -139,7 +149,7 @@ class Tariff(BaseModel):
|
||||
if rub_price <= 0 and stars_price <= 0:
|
||||
raise ValueError(
|
||||
f"period tariff {self.key}: period {months} needs a non-zero rub or stars price" # noqa: E501
|
||||
)
|
||||
)
|
||||
return self
|
||||
|
||||
if not self.traffic_packages or not self.traffic_packages.has_any():
|
||||
@@ -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)])
|
||||
|
||||
Reference in New Issue
Block a user