Rework platform billing: per-plan clocks, grace from period start
Implements BILLING.md. The architecture was sound -- snapshot-on-Due, dated prices, asymmetric dry-run commands are all kept -- so this fixes the three hardcoded assumptions rather than rewriting. The real defect: grace ran from period_END, so an annual club used the whole unpaid year plus 45 days (~410 days) before anything switched it off. Grace now runs from the period START, and every clock is per-plan. - Tier -> Plan (+ TierPrice -> PlanPrice, and every FK). Migration 0004 is hand-written: run non-interactively, makemigrations emits DeleteModel+CreateModel and drops every price, subscription and due. Its two RemoveConstraints must come first, or SQLite's table-rebuild tries to render a constraint over a just-renamed column. Verified by round-tripping real rows through it. - Plan gains duration_months / renewal_lead_days / grace_days / is_trial, with CheckConstraints and a matching clean() so the form reports an impossible plan instead of 500ing on IntegrityError. - Existing dues keep their stored grace_until. Re-deriving it would put the date in the past for every open annual period and archive the entire paying customer base on the next --commit run. - Trials take their length from the trial plan's own duration_months; start_trial() loses its trial_months argument. - New BillingNotice service drives a club-facing warning: every level on the dashboard, and on every management page once urgent. - send_billing_reminders emails club admins, once per escalation level so a daily cron is not a daily email. SMTP settings are env-driven and provider-agnostic; the backend defaults to console. - Paying does not auto-restore an archived club -- the control panel surfaces a Reactivate prompt instead, since a club can also be archived by hand.
This commit is contained in:
@@ -9,6 +9,7 @@ action rather than the whole section (``NewsAuthorRequiredMixin``/``can_add_news
|
||||
|
||||
from waffle import flag_is_active
|
||||
|
||||
from billing.services.notices import club_billing_notice
|
||||
from club.services.access import can_add_news, has_management_access, is_club_admin, is_coach_manager
|
||||
|
||||
#: Every management URL name, mapped to the nav item it should light up --
|
||||
@@ -123,6 +124,23 @@ def is_admin(request):
|
||||
return {"is_club_admin": is_club_admin(request.user, club)}
|
||||
|
||||
|
||||
def billing_notice(request):
|
||||
"""What this club owes the platform, for the club's own admins.
|
||||
|
||||
A context processor rather than view context because the notice has to be able to follow
|
||||
an admin onto every management page once it turns urgent -- billing/base.html renders it
|
||||
at error level only, and the home page renders it at every level.
|
||||
|
||||
Admins only: platform billing is none of an ordinary member's business, and the query is
|
||||
skipped entirely for everyone else rather than fetched and hidden in the template.
|
||||
"""
|
||||
club = getattr(request, "club", None)
|
||||
if club is None or not request.user.is_authenticated or not is_club_admin(request.user, club):
|
||||
return {"billing_notice": None}
|
||||
|
||||
return {"billing_notice": club_billing_notice(club)}
|
||||
|
||||
|
||||
def management_position(request):
|
||||
"""Whether the signed-in user holds a management position (or is ADMIN) --
|
||||
gates the nav's Locations/Opponents links, which ``ManagementPositionRequiredMixin``
|
||||
|
||||
Reference in New Issue
Block a user