fix: separate HWID device renewal flows
Keep one-off device top-ups scoped to the active subscription term and move device renewal into subscription checkout. Carry HWID renewal metadata through provider callbacks and webhooks, including YooKassa saved-card flows. Add admin extension controls, docs, demo data, and regression coverage.
This commit is contained in:
@@ -178,6 +178,7 @@ class SubscriptionLifecycleMixin:
|
||||
user_id: int,
|
||||
target_tariff_key: str,
|
||||
mode: str,
|
||||
payment_id: Optional[int] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
config = self._tariffs_config()
|
||||
if not config:
|
||||
@@ -336,7 +337,7 @@ class SubscriptionLifecycleMixin:
|
||||
"from_tariff_key": before_tariff_key,
|
||||
"to_tariff_key": target.key,
|
||||
"mode": mode,
|
||||
"payment_id": None,
|
||||
"payment_id": payment_id,
|
||||
"days_before": options.get("remaining_days"),
|
||||
"days_after": (updated.end_date - now).days
|
||||
if updated.end_date and target.billing_model == "period"
|
||||
@@ -454,27 +455,11 @@ class SubscriptionLifecycleMixin:
|
||||
user_id,
|
||||
tariff_key,
|
||||
"paid_diff",
|
||||
payment_id=payment_db_id,
|
||||
)
|
||||
if result:
|
||||
sub = await subscription_dal.get_active_subscription_by_user_id(session, user_id)
|
||||
if sub:
|
||||
await tariff_dal.create_tariff_change(
|
||||
session,
|
||||
{
|
||||
"subscription_id": sub.subscription_id,
|
||||
"from_tariff_key": None,
|
||||
"to_tariff_key": tariff_key,
|
||||
"mode": "paid_diff",
|
||||
"payment_id": payment_db_id,
|
||||
"days_before": None,
|
||||
"days_after": (sub.end_date - datetime.now(timezone.utc)).days
|
||||
if sub.end_date
|
||||
else None,
|
||||
"converted_bytes": None,
|
||||
"eff_price_before": None,
|
||||
"eff_price_after": sub.effective_monthly_price_rub,
|
||||
},
|
||||
)
|
||||
result["end_date"] = sub.end_date
|
||||
result["is_active"] = sub.is_active
|
||||
db_user = await user_dal.get_user_by_id(session, user_id)
|
||||
@@ -494,10 +479,29 @@ class SubscriptionLifecycleMixin:
|
||||
await self._record_payment_context(
|
||||
session,
|
||||
payment_db_id,
|
||||
sale_mode=sale_mode_base,
|
||||
sale_mode=sale_mode,
|
||||
tariff_key=tariff.key if tariff else tariff_key,
|
||||
purchased_gb=None,
|
||||
)
|
||||
payment = await payment_dal.get_payment_by_db_id(session, payment_db_id)
|
||||
try:
|
||||
hwid_renewal_devices = int(getattr(payment, "purchased_hwid_devices", 0) or 0)
|
||||
except (TypeError, ValueError):
|
||||
hwid_renewal_devices = 0
|
||||
try:
|
||||
hwid_renewal_price = (
|
||||
float(getattr(payment, "hwid_full_price", 0) or 0)
|
||||
if hwid_renewal_devices > 0
|
||||
else 0.0
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
hwid_renewal_price = 0.0
|
||||
hwid_renewal_valid_from = self._as_aware_utc(
|
||||
getattr(payment, "hwid_valid_from", None) if payment else None
|
||||
)
|
||||
hwid_renewal_valid_until = self._as_aware_utc(
|
||||
getattr(payment, "hwid_valid_until", None) if payment else None
|
||||
)
|
||||
|
||||
db_user = await user_dal.get_user_by_id(session, user_id)
|
||||
if not db_user:
|
||||
@@ -569,6 +573,26 @@ class SubscriptionLifecycleMixin:
|
||||
promo_code_id_from_payment = None
|
||||
|
||||
final_end_date = start_date + timedelta(days=duration_days_total)
|
||||
if hwid_renewal_devices > 0 and hwid_renewal_valid_until and applied_promo_bonus_days:
|
||||
hwid_renewal_valid_until = hwid_renewal_valid_until + timedelta(
|
||||
days=applied_promo_bonus_days
|
||||
)
|
||||
if payment:
|
||||
payment.hwid_valid_until = hwid_renewal_valid_until
|
||||
elif applied_promo_bonus_days > 0 and current_active_sub:
|
||||
try:
|
||||
await tariff_dal.extend_hwid_device_purchases_for_subscription_bonus(
|
||||
session,
|
||||
subscription_id=current_active_sub.subscription_id,
|
||||
at=datetime.now(timezone.utc),
|
||||
subscription_end_before=start_date,
|
||||
delta=timedelta(days=applied_promo_bonus_days),
|
||||
)
|
||||
except Exception:
|
||||
logging.exception(
|
||||
"Failed to extend HWID device purchases for promo payment bonus of user %s",
|
||||
user_id,
|
||||
)
|
||||
await subscription_dal.deactivate_other_active_subscriptions(
|
||||
session, panel_user_uuid, panel_sub_link_id
|
||||
)
|
||||
@@ -614,7 +638,8 @@ class SubscriptionLifecycleMixin:
|
||||
premium_topup_balance_bytes,
|
||||
premium_topup_used_bytes,
|
||||
)
|
||||
effective_monthly_price = float(payment_amount) / max(1, months_int)
|
||||
subscription_amount_for_pricing = max(0.0, float(payment_amount) - hwid_renewal_price)
|
||||
effective_monthly_price = subscription_amount_for_pricing / max(1, months_int)
|
||||
regular_bonus_carry = int(getattr(current_active_sub, "regular_bonus_bytes", 0) or 0)
|
||||
regular_unl_carry = bool(getattr(current_active_sub, "regular_unlimited_override", False))
|
||||
traffic_limit_bytes = self._traffic_limit_for_period_tariff(
|
||||
@@ -698,6 +723,31 @@ class SubscriptionLifecycleMixin:
|
||||
|
||||
final_subscription_url = updated_panel_user.get("subscriptionUrl")
|
||||
final_panel_short_uuid = updated_panel_user.get("shortUuid", panel_short_uuid)
|
||||
hwid_devices_renewed_count = 0
|
||||
hwid_devices_renewed_until = None
|
||||
if hwid_renewal_devices > 0:
|
||||
if (
|
||||
hwid_renewal_valid_from
|
||||
and hwid_renewal_valid_until
|
||||
and hwid_renewal_valid_from < hwid_renewal_valid_until
|
||||
):
|
||||
await tariff_dal.create_hwid_device_purchase(
|
||||
session,
|
||||
subscription_id=new_or_updated_sub.subscription_id,
|
||||
payment_id=payment_db_id,
|
||||
purchased_devices=hwid_renewal_devices,
|
||||
valid_from=hwid_renewal_valid_from,
|
||||
valid_until=hwid_renewal_valid_until,
|
||||
)
|
||||
hwid_devices_renewed_count = hwid_renewal_devices
|
||||
hwid_devices_renewed_until = hwid_renewal_valid_until
|
||||
else:
|
||||
logging.warning(
|
||||
"Skipping HWID renewal purchase for payment %s: invalid window %s -> %s",
|
||||
payment_db_id,
|
||||
hwid_renewal_valid_from,
|
||||
hwid_renewal_valid_until,
|
||||
)
|
||||
|
||||
await self._send_payment_success_email(
|
||||
db_user=db_user,
|
||||
@@ -718,8 +768,12 @@ class SubscriptionLifecycleMixin:
|
||||
"subscription_url": final_subscription_url,
|
||||
"applied_promo_bonus_days": applied_promo_bonus_days,
|
||||
"tariff_key": tariff.key if tariff else None,
|
||||
"hwid_devices_renewal_recommended_count": extra_hwid_devices,
|
||||
"hwid_devices_valid_until": hwid_devices_valid_until,
|
||||
"hwid_devices_renewal_recommended_count": 0
|
||||
if hwid_devices_renewed_count
|
||||
else extra_hwid_devices,
|
||||
"hwid_devices_valid_until": hwid_devices_renewed_until or hwid_devices_valid_until,
|
||||
"hwid_devices_renewed_count": hwid_devices_renewed_count,
|
||||
"hwid_devices_renewed_until": hwid_devices_renewed_until,
|
||||
}
|
||||
|
||||
async def extend_active_subscription_days(
|
||||
@@ -728,6 +782,7 @@ class SubscriptionLifecycleMixin:
|
||||
user_id: int,
|
||||
bonus_days: int,
|
||||
reason: str = "bonus",
|
||||
extend_hwid_devices: bool = True,
|
||||
) -> Optional[datetime]:
|
||||
reason_lower = (reason or "").lower()
|
||||
apply_main_traffic_limit = any(
|
||||
@@ -798,6 +853,21 @@ class SubscriptionLifecycleMixin:
|
||||
updated_sub_model = await subscription_dal.update_subscription_end_date(
|
||||
session, active_sub.subscription_id, new_end_date_obj
|
||||
)
|
||||
if updated_sub_model and extend_hwid_devices:
|
||||
try:
|
||||
await tariff_dal.extend_hwid_device_purchases_for_subscription_bonus(
|
||||
session,
|
||||
subscription_id=active_sub.subscription_id,
|
||||
at=now_utc,
|
||||
subscription_end_before=current_end_date,
|
||||
delta=timedelta(days=bonus_days),
|
||||
)
|
||||
except Exception:
|
||||
logging.exception(
|
||||
"Failed to extend HWID device purchases for %s bonus of user %s",
|
||||
reason,
|
||||
user_id,
|
||||
)
|
||||
|
||||
if (
|
||||
apply_main_traffic_limit
|
||||
|
||||
Reference in New Issue
Block a user