"""Email club admins about platform fees they owe. Reports by default and only sends with --commit, the same posture as archive_overdue_clubs and for a related reason: this one mails paying customers, and a bad clock, a bad import or a rehearsal against production data should cost you a confusing dry-run listing rather than a mailshot you cannot recall. Idempotent across runs by design -- one reminder per due per escalation level, tracked on Due.last_reminder_level -- so a daily cron does not produce a daily email. """ from django.core.management.base import CommandError from billing.services.reminders import reminders_to_send, send_reminder from club.models import Club from features.commands import MaintenanceAwareCommand class Command(MaintenanceAwareCommand): help = "Email club admins about outstanding platform fees (dry run unless --commit)." def add_arguments(self, parser): parser.add_argument("--commit", action="store_true", help="Actually send. Without this the command only reports.") parser.add_argument("--force", action="store_true", help="Re-send even where a reminder already went out at this level.") def handle(self, *args, **options): clubs = Club.objects.active().select_related("subscription", "subscription__plan").order_by("name") results = reminders_to_send(clubs, force=options["force"]) sendable = [result for result in results if result.sent] skipped = [result for result in results if not result.sent] if not results: self.stdout.write(self.style.SUCCESS("Nothing owing. No reminders to send.")) return for result in skipped: style = self.style.WARNING if result.recipients else self.style.ERROR self.stdout.write(style(f"{result.club} — skipped: {result.skipped_reason}")) for result in sendable: self.stdout.write(f"{result.notice.level:>7} · {result.club} — €{result.notice.amount_outstanding} owed, {result.notice.days_until_archive}d to archive → {', '.join(result.recipients)}") if not options["commit"]: self.stdout.write(self.style.WARNING(f"\nDry run: {len(sendable)} reminder(s) would be sent. Re-run with --commit to send them.")) return failures = [] for result in sendable: try: send_reminder(result.club, result.notice, recipients=result.recipients) except OSError as error: # One bad address or a momentary SMTP failure must not stop the rest of the # run: the clubs further down the list are the ones closest to being archived. failures.append(f"{result.club}: {error}") self.stdout.write(self.style.ERROR(f"{result.club} — {error}")) sent = len(sendable) - len(failures) self.stdout.write(self.style.SUCCESS(f"\nSent {sent} reminder(s).")) if failures: # Non-zero so cron mails you: a club that could not be warned is a club that gets # archived without notice. raise CommandError(f"{len(failures)} reminder(s) could not be sent:\n " + "\n ".join(failures))