RemnaWave-bot Update
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user