Closes every club subdomain with a 503 in that club's own colours, stands the scheduled jobs down, and keeps open exactly what is needed to end it again. The exemptions ARE the feature: - /accounts/ stays open on the base domain. Close it too and you cannot sign in to turn maintenance off -- a lock-down with no key, fixable only from a shell. - /healthz answers on every host. Close it and the load balancer decides the node is dead, stops routing to it, and takes the control panel down with everything else. - migrate and collectstatic are NOT blocked. Maintenance is usually declared in order to run them; a blanket guard on BaseCommand would mean turning the mode off to do the work you turned it on for. Only the domain jobs (archive_overdue_clubs, extend_event_series, import_members_csv) refuse, and they exit non-zero so cron mails you -- a scheduled job that silently skips itself is how a month of billing goes missing. The state is cached with a 10-second TTL, not for ever. Write-through makes the flip instant for the shared Redis of a real deployment, and the TTL is the belt to that braces: on a per-process cache -- a dev box with no Redis, or a misconfigured deploy -- a lock-down that reached only one gunicorn worker would be worse than useless. Live-verified: a club subdomain, its login page and the base domain all 503 while the control panel and the sign-in screens stay up. Also adds the two deployment pieces asked for: compose.behind-proxy.yaml for a dev/test box that already runs Caddy on :80 (app on the loopback, host Caddy proxies to it -- and the host's Caddy still needs the DNS plugin, because the wildcard is still a wildcard), and deploy/backup.sh + restore-check.sh with a cron schedule. The backup writes to a .part file and only lands it once gzip -t says it is readable: a truncated dump that looks like a backup is the failure you find on the day you need it. The weekly restore rehearsal is the only line in that cron that proves the rest work. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
39 lines
1.6 KiB
Python
39 lines
1.6 KiB
Python
"""Archive clubs whose billing period has gone unpaid past its grace period.
|
|
|
|
Reports by default and only acts with --commit. That asymmetry is the point: this command
|
|
switches off paying customers, and a cron misconfiguration, a clock skew or a bad import
|
|
should cost you a confusing email, not a morning of angry clubs.
|
|
"""
|
|
|
|
from django.utils import timezone
|
|
|
|
from billing.services.dues import archivable_clubs
|
|
from features.commands import MaintenanceAwareCommand
|
|
|
|
|
|
class Command(MaintenanceAwareCommand):
|
|
help = "Archive clubs that are unpaid past their grace period (dry run unless --commit)."
|
|
|
|
def add_arguments(self, parser):
|
|
parser.add_argument("--commit", action="store_true", help="Actually archive them. Without this the command only reports.")
|
|
|
|
def handle(self, *args, **options):
|
|
today = timezone.localdate()
|
|
overdue = list(archivable_clubs(today))
|
|
|
|
if not overdue:
|
|
self.stdout.write(self.style.SUCCESS("Nothing overdue past grace."))
|
|
return
|
|
|
|
for due in overdue:
|
|
days = (today - due.grace_until).days
|
|
self.stdout.write(f"{due.club} — {due.tier}, {due.balance} owed, grace ended {due.grace_until} ({days} day{'s'[: days != 1]} ago)")
|
|
|
|
if not options["commit"]:
|
|
self.stdout.write(self.style.WARNING(f"\nDry run: {len(overdue)} club(s) would be archived. Re-run with --commit to do it."))
|
|
return
|
|
|
|
for due in overdue:
|
|
due.club.archive()
|
|
self.stdout.write(self.style.SUCCESS(f"\nArchived {len(overdue)} club(s). Their data is kept; restoring re-opens billing."))
|