Checkpoint: management app redesign, onboarding/signup workflow, and events calendar backend

Large uncommitted body of work accumulated across sessions on this branch --
committing as a checkpoint so it's tracked and future worktree-isolated agents
see the real codebase instead of a stale ancestor commit. Covers the
management app's dedicated Tailwind theme and templates, the club onboarding
requirement/signup workflow (club/services/onboarding.py, requirement/status
models, sign-up dashboard), fee/status auto-activation decoupling, referee
management, and the new events calendar grid service layer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
This commit is contained in:
2026-08-19 23:34:43 +02:00
parent bff685966d
commit adf1120358
157 changed files with 20342 additions and 4008 deletions

84
billing/tasks.py Normal file
View File

@@ -0,0 +1,84 @@
"""Celery tasks behind the billing beat schedule entries (see
rosterchief/settings.CELERY_BEAT_SCHEDULE and features/jobs.py).
Each mirrors its management command's *acting* behaviour exactly -- manage.py's own
--dry-run/--commit flags exist for a human at a terminal to preview first (see
billing/management/commands/), which a beat schedule has no terminal to do. These always
act, the same as the crontab entries they replace always passed --commit.
"""
from celery import shared_task
from django.utils import timezone
from billing.services import BillingError
from billing.services.dues import archivable_clubs, renew, subscriptions_due_for_renewal
from billing.services.reminders import reminders_to_send, send_reminder
from club.models import Club
from features.models import Maintenance
def _stand_down():
# Loud, not silent -- see events/tasks.py for why these raise instead of skipping quietly.
raise RuntimeError("Platform is in maintenance mode; this job stood down.")
@shared_task(name="billing.tasks.renew_subscriptions")
def renew_subscriptions():
if Maintenance.is_on():
_stand_down()
due_for_renewal = subscriptions_due_for_renewal()
if not due_for_renewal:
return "Nothing to renew."
renewed, failures = 0, []
for subscription in due_for_renewal:
try:
renew(subscription)
renewed += 1
except BillingError as error:
# One unpriced plan must not stop every other club from being billed.
failures.append(f"{subscription.club}: {error}")
if failures:
raise RuntimeError(f"Renewed {renewed} club(s), {len(failures)} failed:\n " + "\n ".join(failures))
return f"Renewed {renewed} club(s)."
@shared_task(name="billing.tasks.send_billing_reminders")
def send_billing_reminders():
if Maintenance.is_on():
_stand_down()
clubs = Club.objects.active().select_related("subscription", "subscription__plan").order_by("name")
sendable = [result for result in reminders_to_send(clubs) if result.sent]
if not sendable:
return "Nothing owing. No reminders to send."
sent, failures = 0, []
for result in sendable:
try:
send_reminder(result.club, result.notice, recipients=result.recipients)
sent += 1
except OSError as error:
# One bad address or a momentary SMTP failure must not stop the rest of the run.
failures.append(f"{result.club}: {error}")
if failures:
raise RuntimeError(f"Sent {sent} reminder(s), {len(failures)} failed:\n " + "\n ".join(failures))
return f"Sent {sent} reminder(s)."
@shared_task(name="billing.tasks.archive_overdue_clubs")
def archive_overdue_clubs():
if Maintenance.is_on():
_stand_down()
overdue = list(archivable_clubs(timezone.localdate()))
for due in overdue:
due.club.archive()
return f"Archived {len(overdue)} club(s)."