Grace period handling for expired subscriptions. Use when: subscription lapses, payment fails, managing access during grace window.
from datetime import date, timedelta
from django.db import transaction
GRACE_DAYS = {"subscriber": 3, "premium": 7}
def is_in_grace_period(subscription) -> bool:
"""Check if subscription is within grace period."""
if subscription.status != "expired":
return False
grace = GRACE_DAYS.get(subscription.tier.slug, 3)
grace_end = subscription.period_end + timedelta(days=grace)
return date.today() <= grace_end
@transaction.atomic
def process_expired_subscriptions() -> int:
"""Downgrade subscriptions past their grace period."""
from apps.devices.models import QuotaTier
registered = QuotaTier.objects.get(slug="registered")
count = 0
expired = Subscription.objects.select_for_update().filter(
status="expired",
)
for sub in expired:
grace = GRACE_DAYS.get(sub.tier.slug, 3)
grace_end = sub.period_end + timedelta(days=grace)
if date.today() > grace_end:
sub.tier = registered
sub.status = "lapsed"
sub.save(update_fields=["tier", "status", "updated_at"])
count += 1
return count
def user_has_tier_access(user, required_tier_level: int) -> bool:
"""Check tier access, allowing grace period."""
sub = getattr(user, "subscription", None)
if not sub:
return required_tier_level <= 1 # Registered level
if sub.status == "active":
return sub.tier.level >= required_tier_level
if is_in_grace_period(sub):
return sub.tier.level >= required_tier_level
return False
| Bad | Why | Fix |
|---|---|---|
| Immediate downgrade on expiry | Users lose access during payment retry | Grace period |
| Downgrade to Free on lapse | Punishes account holders | Downgrade to Registered |
| No banner during grace period | User doesn't know to renew | Show renewal prompt |
& .\.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