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:
@@ -51,6 +51,15 @@ async def ensure_payment_with_provider_id(
|
||||
description: str,
|
||||
provider: str,
|
||||
provider_payment_id: str,
|
||||
sale_mode: Optional[str] = None,
|
||||
tariff_key: Optional[str] = None,
|
||||
purchased_gb: Optional[float] = None,
|
||||
purchased_hwid_devices: Optional[int] = None,
|
||||
hwid_valid_from: Optional[Any] = None,
|
||||
hwid_valid_until: Optional[Any] = None,
|
||||
hwid_pricing_period_months: Optional[int] = None,
|
||||
hwid_proration_ratio: Optional[float] = None,
|
||||
hwid_full_price: Optional[float] = None,
|
||||
) -> Payment:
|
||||
"""Idempotently create a payment record for a provider event.
|
||||
|
||||
@@ -72,6 +81,20 @@ async def ensure_payment_with_provider_id(
|
||||
"provider_payment_id": provider_payment_id,
|
||||
"provider": provider,
|
||||
}
|
||||
optional_fields = {
|
||||
"sale_mode": sale_mode,
|
||||
"tariff_key": tariff_key,
|
||||
"purchased_gb": purchased_gb,
|
||||
"purchased_hwid_devices": purchased_hwid_devices,
|
||||
"hwid_valid_from": hwid_valid_from,
|
||||
"hwid_valid_until": hwid_valid_until,
|
||||
"hwid_pricing_period_months": hwid_pricing_period_months,
|
||||
"hwid_proration_ratio": hwid_proration_ratio,
|
||||
"hwid_full_price": hwid_full_price,
|
||||
}
|
||||
payment_payload.update(
|
||||
{field: value for field, value in optional_fields.items() if value is not None}
|
||||
)
|
||||
return await create_payment_record(session, payment_payload)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import inspect
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from sqlalchemy import and_, delete, func, or_, select, update
|
||||
@@ -189,6 +189,63 @@ async def expire_hwid_device_purchases(
|
||||
return result.rowcount or 0
|
||||
|
||||
|
||||
def _normalize_aware_utc(value: datetime) -> datetime:
|
||||
if value.tzinfo is None:
|
||||
return value.replace(tzinfo=timezone.utc)
|
||||
return value
|
||||
|
||||
|
||||
async def extend_hwid_device_purchases_for_subscription_bonus(
|
||||
session: AsyncSession,
|
||||
*,
|
||||
subscription_id: int,
|
||||
at: Optional[datetime] = None,
|
||||
subscription_end_before: Optional[datetime] = None,
|
||||
delta: timedelta,
|
||||
) -> int:
|
||||
if delta.total_seconds() <= 0:
|
||||
return 0
|
||||
at = _normalize_aware_utc(at or datetime.now(timezone.utc))
|
||||
end_before = _normalize_aware_utc(subscription_end_before) if subscription_end_before else None
|
||||
|
||||
target_records: List[HwidDevicePurchase] = []
|
||||
if end_before:
|
||||
tail_result = await session.execute(
|
||||
select(HwidDevicePurchase).where(
|
||||
and_(
|
||||
HwidDevicePurchase.subscription_id == subscription_id,
|
||||
HwidDevicePurchase.purchased_devices > 0,
|
||||
HwidDevicePurchase.valid_until.is_not(None),
|
||||
HwidDevicePurchase.valid_until >= end_before,
|
||||
HwidDevicePurchase.valid_until > at,
|
||||
or_(
|
||||
HwidDevicePurchase.valid_from.is_(None),
|
||||
HwidDevicePurchase.valid_from < end_before,
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
target_records = list(tail_result.scalars().all())
|
||||
|
||||
if not target_records:
|
||||
active_result = await session.execute(
|
||||
select(HwidDevicePurchase).where(
|
||||
and_(
|
||||
*_hwid_active_conditions(subscription_id, at),
|
||||
HwidDevicePurchase.valid_until.is_not(None),
|
||||
)
|
||||
)
|
||||
)
|
||||
target_records = list(active_result.scalars().all())
|
||||
|
||||
for record in target_records:
|
||||
if record.valid_until is not None:
|
||||
record.valid_until = _normalize_aware_utc(record.valid_until) + delta
|
||||
if target_records:
|
||||
await session.flush()
|
||||
return len(target_records)
|
||||
|
||||
|
||||
async def create_tariff_change(
|
||||
session: AsyncSession,
|
||||
change_data: Dict[str, Any],
|
||||
|
||||
Reference in New Issue
Block a user