55 lines
2.5 KiB
Python
Executable File
55 lines
2.5 KiB
Python
Executable File
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"])
|