RemnaWave-bot Update

This commit is contained in:
2026-05-06 15:41:56 +03:00
parent ce7a1f70b2
commit 2cc2f12a21
22 changed files with 3087 additions and 129 deletions

View File

@@ -31,9 +31,31 @@ REFERRAL_BONUS_APPLIED_STATUS = "APPLIED"
logger = logging.getLogger(__name__)
@dataclass(slots=True)
class AppliedDiscount:
code: str = ""
source: str = ""
discount_percent: int = 0
plan_code: str | None = None
expires_at: datetime | None = None
def applies_to_plan(self, plan_code: str) -> bool:
if not self.code:
return False
if self.plan_code is None:
return True
# "all_30" matches any product with 30 days (vpn_white_30, white_30, vpn_30)
if self.plan_code.startswith("all_"):
suffix = self.plan_code.removeprefix("all") # "_30"
return plan_code.endswith(suffix)
# Exact match or product-prefix match (e.g. promo plan_code="vpn_white" matches plan "vpn_white_30")
return self.plan_code == plan_code or plan_code.startswith(f"{self.plan_code}_")
@dataclass(slots=True)
class PaymentPlan:
code: str
label: str
title: str
description: str
duration_days: int
@@ -41,6 +63,9 @@ class PaymentPlan:
discount_percent: int
original_amount_rub: int
applied_referral_code: str = ""
discount_code: str = ""
discount_source: str = ""
discount_scope_plan_code: str | None = None
traffic_limit_bytes: int = 0
traffic_limit_strategy: str = "NO_RESET"
internal_squad_uuids: list[str] | None = None
@@ -130,14 +155,14 @@ class PaymentService:
async def get_available_plans(self, *, telegram_id: int) -> list[PaymentPlan]:
config = await self._get_config()
applied_referral_code = await self._get_applied_referral_code(
applied_discount = await self._get_applied_discount(
telegram_id,
config=config,
)
return [
self._build_plan(
plan,
applied_referral_code=applied_referral_code,
applied_discount=applied_discount,
config=config,
)
for plan in config.payment_plans
@@ -154,13 +179,13 @@ class PaymentService:
plan_code: str,
) -> CreatedPaymentOrder:
config = await self._get_config()
applied_referral_code = await self._get_applied_referral_code(
applied_discount = await self._get_applied_discount(
telegram_id,
config=config,
)
plan = self._get_plan_by_code(
plan_code,
applied_referral_code=applied_referral_code,
applied_discount=applied_discount,
config=config,
)
if not plan.is_ready:
@@ -203,7 +228,7 @@ class PaymentService:
status=PENDING_PAYMENT_STATUS,
invoice_payload=invoice_payload,
provision_username=provision_username,
error_message=applied_referral_code or None,
error_message=plan.discount_code or None,
)
)
await session.commit()
@@ -293,10 +318,15 @@ class PaymentService:
async def approve_order(self, *, order_uuid: str) -> IssuedAccess:
order = await self._claim_order_for_approval(order_uuid)
config = await self._get_config()
is_renewal = False
try:
config = await self._get_config()
plan = self._get_plan_by_code(
order.plan_code,
applied_discount=AppliedDiscount(),
config=config,
)
remote_user: RemnawaveUser | None = None
if order.remnawave_user_uuid:
try:
@@ -324,19 +354,39 @@ class PaymentService:
remote_user = self._select_referral_bonus_target(remote_users)
if remote_user is None:
remote_user = await self._create_remnawave_user(order, config=config)
remote_user = await self._create_remnawave_user(
order,
plan=plan,
config=config,
)
else:
is_renewal = True
remote_user = await self._extend_remnawave_user(
order,
remote_user,
plan=plan,
config=config,
)
except ValueError as exc:
await self._restore_review_after_failed_approval(order.order_uuid, str(exc))
raise
except RemnawaveApiError as exc:
await self._restore_review_after_failed_approval(order.order_uuid, exc.message)
raise
await self._save_fulfilled_order(order.order_uuid, remote_user)
# Burn the applied discount (promo or referral) so it cannot be reused
try:
await self._sync_service.consume_discount_for_user(order.telegram_id)
except Exception:
logger.warning(
"Could not consume discount for telegram_id=%s after order=%s",
order.telegram_id,
order.order_uuid,
exc_info=True,
)
own_bonus = None
try:
own_bonus = await self.apply_pending_referral_bonuses(
@@ -422,9 +472,14 @@ class PaymentService:
"",
f"Сумма к переводу: <b>{order.plan.amount_rub} ₽</b>",
]
if order.plan.applied_referral_code:
if order.plan.discount_code:
discount_label = (
"Промокод"
if order.plan.discount_source == "promo"
else "Реферальный код"
)
lines.append(
f"Реферальный код: <code>{order.plan.applied_referral_code}</code> "
f"{discount_label}: <code>{order.plan.discount_code}</code> "
f"(<b>-{order.plan.discount_percent}%</b>)"
)
if order.extends_existing_access:
@@ -458,14 +513,14 @@ class PaymentService:
self,
plan_code: str,
*,
applied_referral_code: str,
applied_discount: AppliedDiscount,
config: BotConfigSnapshot,
) -> PaymentPlan:
for plan in config.payment_plans:
if plan.code == plan_code:
return self._build_plan(
plan,
applied_referral_code=applied_referral_code,
applied_discount=applied_discount,
config=config,
)
@@ -475,10 +530,15 @@ class PaymentService:
self,
plan_settings: PaymentPlanSettings,
*,
applied_referral_code: str,
applied_discount: AppliedDiscount,
config: BotConfigSnapshot,
) -> PaymentPlan:
discount_percent = max(config.referral_discount_percent, 0) if applied_referral_code else 0
resolved_discount = (
applied_discount
if applied_discount.applies_to_plan(plan_settings.code)
else AppliedDiscount()
)
discount_percent = max(resolved_discount.discount_percent, 0)
discounted_amount = self._apply_discount(
amount_rub=plan_settings.amount_rub,
discount_percent=discount_percent,
@@ -486,36 +546,105 @@ class PaymentService:
traffic_limit_bytes = max(config.payment_plan_traffic_limit_gb, 0) * 1024**3
return PaymentPlan(
code=plan_settings.code,
title=f"{config.bot_brand_name} на {plan_settings.days} дней",
description=f"Доступ к VPN на {plan_settings.days} дней",
label=plan_settings.title or f"{plan_settings.days} дней",
title=(
f"{config.bot_brand_name} · {plan_settings.title}"
if plan_settings.title
else f"{config.bot_brand_name} на {plan_settings.days} дней"
),
description=plan_settings.description or f"Доступ к VPN на {plan_settings.days} дней",
duration_days=plan_settings.days,
amount_rub=discounted_amount,
discount_percent=discount_percent,
original_amount_rub=plan_settings.amount_rub,
applied_referral_code=applied_referral_code,
applied_referral_code=(
resolved_discount.code if resolved_discount.source == "referral" else ""
),
discount_code=resolved_discount.code,
discount_source=resolved_discount.source,
discount_scope_plan_code=resolved_discount.plan_code,
traffic_limit_bytes=traffic_limit_bytes,
traffic_limit_strategy=config.payment_plan_traffic_reset_period.strip() or "NO_RESET",
internal_squad_uuids=config.payment_internal_squad_uuids,
internal_squad_uuids=self._resolve_plan_internal_squad_uuids(
plan_settings,
config=config,
),
external_squad_uuid=config.payment_external_squad_uuid_normalized,
)
@staticmethod
def _resolve_plan_internal_squad_uuids(
plan_settings: PaymentPlanSettings,
*,
config: BotConfigSnapshot,
) -> list[str]:
if not plan_settings.squad_groups:
return config.payment_internal_squad_uuids
values: list[str] = []
for group in plan_settings.squad_groups:
if group == "vpn":
group_values = config.payment_vpn_squad_uuids
elif group == "white":
group_values = config.payment_white_squad_uuids
else:
group_values = []
for squad_uuid in group_values:
if squad_uuid not in values:
values.append(squad_uuid)
return values
@staticmethod
def _apply_discount(*, amount_rub: int, discount_percent: int) -> int:
if discount_percent <= 0:
return amount_rub
return max(1, ceil(amount_rub * (100 - discount_percent) / 100))
async def _get_applied_referral_code(
async def get_active_discount_for_user(self, *, telegram_id: int) -> AppliedDiscount:
config = await self._get_config()
return await self._get_applied_discount(telegram_id, config=config)
async def _get_applied_discount(
self,
telegram_id: int,
*,
config: BotConfigSnapshot | None = None,
) -> str:
) -> AppliedDiscount:
resolved_config = config or await self._get_config()
promo_getter = getattr(self._sync_service, "get_active_promo_for_user", None)
if callable(promo_getter):
promo = await promo_getter(telegram_id)
if promo is not None:
return AppliedDiscount(
code=str(promo.code).strip().upper(),
source="promo",
discount_percent=max(int(promo.discount_percent), 0),
plan_code=promo.plan_code,
expires_at=promo.expires_at,
)
if not resolved_config.referral_enabled:
return ""
return AppliedDiscount()
summary = await self._sync_service.get_referral_summary(telegram_id)
return summary.applied_referral_code.strip().upper()
referral_code = summary.applied_referral_code.strip().upper()
if not referral_code:
return AppliedDiscount()
# Check if referral discount was already consumed by a previous payment
discount_used_checker = getattr(
self._sync_service, "is_referral_discount_used", None
)
if callable(discount_used_checker) and await discount_used_checker(telegram_id):
return AppliedDiscount()
return AppliedDiscount(
code=referral_code,
source="referral",
discount_percent=max(resolved_config.referral_discount_percent, 0),
)
async def apply_pending_referral_bonuses(
self,
@@ -655,6 +784,7 @@ class PaymentService:
self,
order: PaymentOrder,
*,
plan: PaymentPlan,
config: BotConfigSnapshot,
) -> RemnawaveUser:
expire_at = datetime.now(timezone.utc) + timedelta(days=order.plan_duration_days)
@@ -665,7 +795,7 @@ class PaymentService:
"trafficLimitStrategy": order.traffic_limit_strategy,
"telegramId": order.telegram_id,
"description": f"Created by Telegram bot manual payment {order.order_uuid}",
"activeInternalSquads": config.payment_internal_squad_uuids,
"activeInternalSquads": plan.internal_squad_uuids or [],
}
if config.payment_user_tag_normalized:
body["tag"] = config.payment_user_tag_normalized
@@ -678,6 +808,7 @@ class PaymentService:
order: PaymentOrder,
remote_user: RemnawaveUser,
*,
plan: PaymentPlan,
config: BotConfigSnapshot,
) -> RemnawaveUser:
current_expire_at = remote_user.expire_at
@@ -699,7 +830,7 @@ class PaymentService:
"trafficLimitStrategy": order.traffic_limit_strategy,
"telegramId": order.telegram_id,
"description": f"Updated by Telegram bot manual payment {order.order_uuid}",
"activeInternalSquads": config.payment_internal_squad_uuids,
"activeInternalSquads": plan.internal_squad_uuids or [],
}
if config.payment_user_tag_normalized:
body["tag"] = config.payment_user_tag_normalized