Add addon discount toggle for promo groups

This commit is contained in:
Egor
2025-09-25 13:03:31 +03:00
parent 3df541689b
commit 80efd4ab86
9 changed files with 182 additions and 4 deletions

View File

@@ -60,6 +60,7 @@ async def create_promo_group(
device_discount_percent: int,
period_discounts: Optional[Dict[int, int]] = None,
auto_assign_total_spent_kopeks: Optional[int] = None,
apply_discounts_to_addons: bool = True,
) -> PromoGroup:
normalized_period_discounts = _normalize_period_discounts(period_discounts)
@@ -76,6 +77,7 @@ async def create_promo_group(
device_discount_percent=max(0, min(100, device_discount_percent)),
period_discounts=normalized_period_discounts or None,
auto_assign_total_spent_kopeks=auto_assign_total_spent_kopeks,
apply_discounts_to_addons=bool(apply_discounts_to_addons),
is_default=False,
)
@@ -84,13 +86,14 @@ async def create_promo_group(
await db.refresh(promo_group)
logger.info(
"Создана промогруппа '%s' с скидками (servers=%s%%, traffic=%s%%, devices=%s%%, periods=%s) и порогом автоприсвоения %s",
"Создана промогруппа '%s' с скидками (servers=%s%%, traffic=%s%%, devices=%s%%, periods=%s) и порогом автоприсвоения %s, скидки на доп. услуги: %s",
promo_group.name,
promo_group.server_discount_percent,
promo_group.traffic_discount_percent,
promo_group.device_discount_percent,
normalized_period_discounts,
(auto_assign_total_spent_kopeks or 0) / 100,
"on" if promo_group.apply_discounts_to_addons else "off",
)
return promo_group
@@ -106,6 +109,7 @@ async def update_promo_group(
device_discount_percent: Optional[int] = None,
period_discounts: Optional[Dict[int, int]] = None,
auto_assign_total_spent_kopeks: Optional[int] = None,
apply_discounts_to_addons: Optional[bool] = None,
) -> PromoGroup:
if name is not None:
group.name = name.strip()
@@ -120,6 +124,8 @@ async def update_promo_group(
group.period_discounts = normalized_period_discounts or None
if auto_assign_total_spent_kopeks is not None:
group.auto_assign_total_spent_kopeks = max(0, auto_assign_total_spent_kopeks)
if apply_discounts_to_addons is not None:
group.apply_discounts_to_addons = bool(apply_discounts_to_addons)
await db.commit()
await db.refresh(group)

View File

@@ -292,6 +292,7 @@ class PromoGroup(Base):
device_discount_percent = Column(Integer, nullable=False, default=0)
period_discounts = Column(JSON, nullable=True, default=dict)
auto_assign_total_spent_kopeks = Column(Integer, nullable=True, default=None)
apply_discounts_to_addons = Column(Boolean, nullable=False, default=True)
is_default = Column(Boolean, nullable=False, default=False)
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())

View File

@@ -931,6 +931,54 @@ async def ensure_promo_groups_setup():
"Добавлена колонка promo_groups.auto_assign_total_spent_kopeks"
)
addon_discount_column_exists = await check_column_exists(
"promo_groups", "apply_discounts_to_addons"
)
if not addon_discount_column_exists:
if db_type == "sqlite":
await conn.execute(
text(
"ALTER TABLE promo_groups ADD COLUMN apply_discounts_to_addons BOOLEAN NOT NULL DEFAULT 1"
)
)
await conn.execute(
text(
"UPDATE promo_groups SET apply_discounts_to_addons = 1 WHERE apply_discounts_to_addons IS NULL"
)
)
elif db_type == "postgresql":
await conn.execute(
text(
"ALTER TABLE promo_groups ADD COLUMN apply_discounts_to_addons BOOLEAN NOT NULL DEFAULT TRUE"
)
)
await conn.execute(
text(
"UPDATE promo_groups SET apply_discounts_to_addons = TRUE WHERE apply_discounts_to_addons IS NULL"
)
)
elif db_type == "mysql":
await conn.execute(
text(
"ALTER TABLE promo_groups ADD COLUMN apply_discounts_to_addons TINYINT(1) NOT NULL DEFAULT 1"
)
)
await conn.execute(
text(
"UPDATE promo_groups SET apply_discounts_to_addons = 1 WHERE apply_discounts_to_addons IS NULL"
)
)
else:
logger.error(
f"Неподдерживаемый тип БД для promo_groups.apply_discounts_to_addons: {db_type}"
)
return False
logger.info(
"Добавлена колонка promo_groups.apply_discounts_to_addons"
)
column_exists = await check_column_exists("users", "promo_group_id")
if not column_exists:
@@ -1994,6 +2042,7 @@ async def check_migration_status():
"users_promo_group_column": False,
"promo_groups_period_discounts_column": False,
"promo_groups_auto_assign_column": False,
"promo_groups_addon_discount_column": False,
"users_auto_promo_group_assigned_column": False,
"subscription_crypto_link_column": False,
}
@@ -2011,6 +2060,7 @@ async def check_migration_status():
status["users_promo_group_column"] = await check_column_exists('users', 'promo_group_id')
status["promo_groups_period_discounts_column"] = await check_column_exists('promo_groups', 'period_discounts')
status["promo_groups_auto_assign_column"] = await check_column_exists('promo_groups', 'auto_assign_total_spent_kopeks')
status["promo_groups_addon_discount_column"] = await check_column_exists('promo_groups', 'apply_discounts_to_addons')
status["users_auto_promo_group_assigned_column"] = await check_column_exists('users', 'auto_promo_group_assigned')
status["subscription_crypto_link_column"] = await check_column_exists('subscriptions', 'subscription_crypto_link')
@@ -2048,6 +2098,7 @@ async def check_migration_status():
"users_promo_group_column": "Колонка promo_group_id у пользователей",
"promo_groups_period_discounts_column": "Колонка period_discounts у промо-групп",
"promo_groups_auto_assign_column": "Колонка auto_assign_total_spent_kopeks у промо-групп",
"promo_groups_addon_discount_column": "Колонка apply_discounts_to_addons у промо-групп",
"users_auto_promo_group_assigned_column": "Флаг автоназначения промогруппы у пользователей",
"subscription_crypto_link_column": "Колонка subscription_crypto_link в subscriptions",
}

View File

@@ -39,6 +39,32 @@ def _format_discount_line(texts, group) -> str:
)
def _format_addon_discounts_line(texts, group: PromoGroup) -> str:
enabled = getattr(group, "apply_discounts_to_addons", True)
if enabled:
return texts.t(
"ADMIN_PROMO_GROUP_ADDON_DISCOUNT_ENABLED",
"Скидки на доп. услуги: включены",
)
return texts.t(
"ADMIN_PROMO_GROUP_ADDON_DISCOUNT_DISABLED",
"Скидки на доп. услуги: отключены",
)
def _get_addon_discounts_button_text(texts, group: PromoGroup) -> str:
enabled = getattr(group, "apply_discounts_to_addons", True)
if enabled:
return texts.t(
"ADMIN_PROMO_GROUP_TOGGLE_ADDON_DISCOUNT_DISABLE",
"🧩 Отключить скидки на доп. услуги",
)
return texts.t(
"ADMIN_PROMO_GROUP_TOGGLE_ADDON_DISCOUNT_ENABLE",
"🧩 Включить скидки на доп. услуги",
)
def _normalize_periods_dict(raw: Optional[Dict]) -> Dict[int, int]:
if not raw or not isinstance(raw, dict):
return {}
@@ -257,6 +283,7 @@ def _build_edit_menu_content(
lines = [
header,
_format_discount_line(texts, group),
_format_addon_discounts_line(texts, group),
_format_auto_assign_line(texts, group),
]
@@ -318,6 +345,12 @@ def _build_edit_menu_content(
callback_data=f"promo_group_edit_field_{group.id}_periods",
)
],
[
types.InlineKeyboardButton(
text=_get_addon_discounts_button_text(texts, group),
callback_data=f"promo_group_toggle_addons_{group.id}",
)
],
[
types.InlineKeyboardButton(
text=texts.t(
@@ -1192,6 +1225,45 @@ async def delete_promo_group_confirmed(
await callback.answer()
@admin_required
@error_handler
async def toggle_promo_group_addon_discounts(
callback: types.CallbackQuery,
db_user,
db: AsyncSession,
):
group = await _get_group_or_alert(callback, db)
if not group:
return
texts = get_texts(db_user.language)
new_value = not getattr(group, "apply_discounts_to_addons", True)
group = await update_promo_group(
db,
group,
apply_discounts_to_addons=new_value,
)
status_text = texts.t(
"ADMIN_PROMO_GROUP_ADDON_DISCOUNT_UPDATED_ENABLED"
if new_value
else "ADMIN_PROMO_GROUP_ADDON_DISCOUNT_UPDATED_DISABLED",
"Скидки на докупку доп. услуг {status}.",
).format(status="включены" if new_value else "отключены")
await _send_edit_menu_after_update(
callback.message,
texts,
group,
db_user.language,
status_text,
)
await callback.answer()
def register_handlers(dp: Dispatcher):
dp.callback_query.register(show_promo_groups_menu, F.data == "admin_promo_groups")
dp.callback_query.register(show_promo_group_details, F.data.startswith("promo_group_manage_"))
@@ -1200,6 +1272,10 @@ def register_handlers(dp: Dispatcher):
prompt_edit_promo_group_field,
F.data.startswith("promo_group_edit_field_"),
)
dp.callback_query.register(
toggle_promo_group_addon_discounts,
F.data.startswith("promo_group_toggle_addons_"),
)
dp.callback_query.register(
start_edit_promo_group,
F.data.regexp(r"^promo_group_edit_\d+$"),

View File

@@ -138,6 +138,12 @@
"ADMIN_PROMO_GROUPS_TITLE": "💳 <b>Promo groups</b>",
"ADMIN_PROMO_GROUPS_SUMMARY": "Groups total: {count}\nMembers total: {members}",
"ADMIN_PROMO_GROUPS_DISCOUNTS": "Discounts — servers: {servers}%, traffic: {traffic}%, devices: {devices}%",
"ADMIN_PROMO_GROUP_ADDON_DISCOUNT_ENABLED": "Add-on discounts: enabled",
"ADMIN_PROMO_GROUP_ADDON_DISCOUNT_DISABLED": "Add-on discounts: disabled",
"ADMIN_PROMO_GROUP_TOGGLE_ADDON_DISCOUNT_ENABLE": "🧩 Enable add-on discounts",
"ADMIN_PROMO_GROUP_TOGGLE_ADDON_DISCOUNT_DISABLE": "🧩 Disable add-on discounts",
"ADMIN_PROMO_GROUP_ADDON_DISCOUNT_UPDATED_ENABLED": "Add-on purchase discounts have been enabled.",
"ADMIN_PROMO_GROUP_ADDON_DISCOUNT_UPDATED_DISABLED": "Add-on purchase discounts have been disabled.",
"ADMIN_PROMO_GROUPS_DEFAULT_LABEL": " (default)",
"ADMIN_PROMO_GROUPS_MEMBERS_COUNT": "Members: {count}",
"ADMIN_PROMO_GROUPS_EMPTY": "No promo groups found.",

View File

@@ -15,6 +15,12 @@
"ADMIN_PROMO_GROUPS_TITLE": "💳 <b>Промогруппы</b>",
"ADMIN_PROMO_GROUPS_SUMMARY": "Всего групп: {count}\nВсего участников: {members}",
"ADMIN_PROMO_GROUPS_DISCOUNTS": "Скидки — серверы: {servers}%, трафик: {traffic}%, устройства: {devices}%",
"ADMIN_PROMO_GROUP_ADDON_DISCOUNT_ENABLED": "Скидки на доп. услуги: включены",
"ADMIN_PROMO_GROUP_ADDON_DISCOUNT_DISABLED": "Скидки на доп. услуги: отключены",
"ADMIN_PROMO_GROUP_TOGGLE_ADDON_DISCOUNT_ENABLE": "🧩 Включить скидки на доп. услуги",
"ADMIN_PROMO_GROUP_TOGGLE_ADDON_DISCOUNT_DISABLE": "🧩 Отключить скидки на доп. услуги",
"ADMIN_PROMO_GROUP_ADDON_DISCOUNT_UPDATED_ENABLED": "Скидки на докупку доп. услуг включены.",
"ADMIN_PROMO_GROUP_ADDON_DISCOUNT_UPDATED_DISABLED": "Скидки на докупку доп. услуг отключены.",
"ADMIN_PROMO_GROUPS_DEFAULT_LABEL": " (базовая)",
"ADMIN_PROMO_GROUPS_MEMBERS_COUNT": "Участников: {count}",
"ADMIN_PROMO_GROUPS_EMPTY": "Промогруппы не найдены.",

View File

@@ -38,6 +38,26 @@ def _resolve_discount_percent(
return 0
def _resolve_addon_discount_percent(
user: Optional[User],
promo_group: Optional[PromoGroup],
category: str,
*,
period_days: Optional[int] = None,
) -> int:
group = promo_group or (getattr(user, "promo_group", None) if user else None)
if group is not None and not getattr(group, "apply_discounts_to_addons", True):
return 0
return _resolve_discount_percent(
user,
promo_group,
category,
period_days=period_days,
)
def get_traffic_reset_strategy():
from app.config import settings
strategy = settings.DEFAULT_TRAFFIC_RESET_STRATEGY.upper()
@@ -858,7 +878,7 @@ class SubscriptionService:
if additional_traffic_gb > 0:
traffic_price_per_month = settings.get_traffic_price(additional_traffic_gb)
traffic_discount_percent = _resolve_discount_percent(
traffic_discount_percent = _resolve_addon_discount_percent(
user,
promo_group,
"traffic",
@@ -881,7 +901,7 @@ class SubscriptionService:
if additional_devices > 0:
devices_price_per_month = additional_devices * settings.PRICE_PER_DEVICE
devices_discount_percent = _resolve_discount_percent(
devices_discount_percent = _resolve_addon_discount_percent(
user,
promo_group,
"devices",
@@ -908,7 +928,7 @@ class SubscriptionService:
server = await get_server_squad_by_id(db, server_id)
if server and server.is_available:
server_price_per_month = server.price_kopeks
servers_discount_percent = _resolve_discount_percent(
servers_discount_percent = _resolve_addon_discount_percent(
user,
promo_group,
"servers",

View File

@@ -151,6 +151,12 @@
"ADMIN_PROMO_GROUPS_TITLE": "💳 <b>Promo groups</b>",
"ADMIN_PROMO_GROUPS_SUMMARY": "Groups total: {count}\nMembers total: {members}",
"ADMIN_PROMO_GROUPS_DISCOUNTS": "Discounts — servers: {servers}%, traffic: {traffic}%, devices: {devices}%",
"ADMIN_PROMO_GROUP_ADDON_DISCOUNT_ENABLED": "Add-on discounts: enabled",
"ADMIN_PROMO_GROUP_ADDON_DISCOUNT_DISABLED": "Add-on discounts: disabled",
"ADMIN_PROMO_GROUP_TOGGLE_ADDON_DISCOUNT_ENABLE": "🧩 Enable add-on discounts",
"ADMIN_PROMO_GROUP_TOGGLE_ADDON_DISCOUNT_DISABLE": "🧩 Disable add-on discounts",
"ADMIN_PROMO_GROUP_ADDON_DISCOUNT_UPDATED_ENABLED": "Add-on purchase discounts have been enabled.",
"ADMIN_PROMO_GROUP_ADDON_DISCOUNT_UPDATED_DISABLED": "Add-on purchase discounts have been disabled.",
"ADMIN_PROMO_GROUP_PERIOD_DISCOUNTS_HEADER": "⏳ Period discounts:",
"ADMIN_PROMO_GROUPS_DEFAULT_LABEL": " (default)",
"ADMIN_PROMO_GROUPS_MEMBERS_COUNT": "Members: {count}",

View File

@@ -17,6 +17,12 @@
"ADMIN_PROMO_GROUPS_TITLE": "💳 <b>Промогруппы</b>",
"ADMIN_PROMO_GROUPS_SUMMARY": "Всего групп: {count}\nВсего участников: {members}",
"ADMIN_PROMO_GROUPS_DISCOUNTS": "Скидки — серверы: {servers}%, трафик: {traffic}%, устройства: {devices}%",
"ADMIN_PROMO_GROUP_ADDON_DISCOUNT_ENABLED": "Скидки на доп. услуги: включены",
"ADMIN_PROMO_GROUP_ADDON_DISCOUNT_DISABLED": "Скидки на доп. услуги: отключены",
"ADMIN_PROMO_GROUP_TOGGLE_ADDON_DISCOUNT_ENABLE": "🧩 Включить скидки на доп. услуги",
"ADMIN_PROMO_GROUP_TOGGLE_ADDON_DISCOUNT_DISABLE": "🧩 Отключить скидки на доп. услуги",
"ADMIN_PROMO_GROUP_ADDON_DISCOUNT_UPDATED_ENABLED": "Скидки на докупку доп. услуг включены.",
"ADMIN_PROMO_GROUP_ADDON_DISCOUNT_UPDATED_DISABLED": "Скидки на докупку доп. услуг отключены.",
"ADMIN_PROMO_GROUP_PERIOD_DISCOUNTS_HEADER": "⏳ Скидки по периодам:",
"ADMIN_PROMO_GROUPS_DEFAULT_LABEL": " (базовая)",
"ADMIN_PROMO_GROUPS_MEMBERS_COUNT": "Участников: {count}",