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, ProfileEmailChange, RateWindow, Registration, WebhookEvent logger = logging.getLogger(__name__) class Command(BaseCommand): help = "Reconcile Stripe/SHKeeper payments 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() ProfileEmailChange.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"])