Memberships

This commit is contained in:
2026-10-23 12:02:50 -04:00
parent 87dd5453e4
commit 6cf0f0525f
42 changed files with 2145 additions and 7 deletions
+53
View File
@@ -0,0 +1,53 @@
import logging
import time
from datetime import timedelta
from django.core.management.base import BaseCommand, CommandError
from django.contrib.sessions.models import Session
from django.utils import timezone
from members.billing import sync_account
from members.models import Account, RateWindow, Registration, WebhookEvent
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = "Reconcile Stripe subscriptions and LDAP groups, including offline expiry."
def add_arguments(self, parser):
parser.add_argument("--loop", action="store_true")
parser.add_argument("--interval", type=int, default=60)
def handle(self, *args, **options):
if options["interval"] < 10:
raise CommandError("Use an interval of at least 10 seconds.")
while True:
failed = 0
# Expire EVERY account before any potentially slow Stripe history
# reads. This pass requires only the local DB and LDAP.
for account_id in Account.objects.values_list("pk", flat=True).iterator():
try:
sync_account(account_id, refresh_stripe=False)
except Exception as exc:
failed += 1
logger.error("Local expiry sync failed account=%s kind=%s", account_id, type(exc).__name__)
# A complete sweep is intentional: webhook loss must not prevent
# renewal recovery or revocation. Each account has its own lock.
for account_id in Account.objects.values_list("pk", flat=True).iterator():
try:
if not sync_account(account_id):
failed += 1
except Exception as exc:
failed += 1
logger.error("Membership sync failed account=%s kind=%s", account_id, type(exc).__name__)
now = timezone.now()
Session.objects.filter(expire_date__lte=now).delete()
Registration.objects.filter(expires_at__lte=now).delete()
RateWindow.objects.filter(expires_at__lte=now).delete()
WebhookEvent.objects.filter(processed_at__lt=now - timedelta(days=30)).delete()
if not options["loop"]:
if failed:
raise CommandError(f"{failed} account(s) failed to synchronize; inspect worker logs.")
return
time.sleep(options["interval"])