Tier downgrade: grace period, feature revocation. Use when: user downgrades subscription, handling feature access reduction, scheduling end-of-cycle changes.
pending_tier and apply on renewal datefrom django.db import transaction
from apps.devices.models import QuotaTier
@transaction.atomic
def schedule_downgrade(user_id: int, new_tier_slug: str) -> dict:
"""Schedule downgrade for end of current billing period."""
subscription = Subscription.objects.select_for_update().get(user_id=user_id)
new_tier = QuotaTier.objects.get(slug=new_tier_slug)
if new_tier.level >= subscription.tier.level:
raise ValueError("Downgrade must be to a lower tier")
subscription.pending_tier = new_tier
subscription.pending_change_date = subscription.period_end
subscription.save(update_fields=["pending_tier", "pending_change_date",
"updated_at"])
return {
"current_tier": subscription.tier.slug,
"pending_tier": new_tier.slug,
"effective_date": subscription.period_end,
}
@transaction.atomic
def apply_pending_downgrade(subscription_id: int) -> bool:
"""Apply scheduled downgrade. Called by Celery on period_end."""
sub = Subscription.objects.select_for_update().get(pk=subscription_id)
if not sub.pending_tier:
return False
sub.tier = sub.pending_tier
sub.pending_tier = None
sub.pending_change_date = None
sub.save(update_fields=["tier", "pending_tier",
"pending_change_date", "updated_at"])
return True
@transaction.atomic
def cancel_pending_downgrade(user_id: int) -> bool:
"""Cancel a scheduled downgrade before it takes effect."""
sub = Subscription.objects.select_for_update().get(user_id=user_id)
if not sub.pending_tier:
return False
sub.pending_tier = None
sub.pending_change_date = None
sub.save(update_fields=["pending_tier", "pending_change_date", "updated_at"])
return True
| Bad | Why | Fix |
|---|---|---|
| Immediate feature revocation on downgrade | User paid for current period | Apply at period end |
| No way to cancel pending downgrade | Bad UX | Allow cancellation |
| Revoking downloads already in progress | Data loss | Only restrict new actions |
pending_tier field on subscription model& .\.venv\Scripts\python.exe -m ruff check . --fix
& .\.venv\Scripts\python.exe -m ruff format .
& .\.venv\Scripts\python.exe manage.py check --settings=app.settings_dev