Match tests to the reworked dashboard, surface the renewals KPI
The club table was rebuilt (logo, status badges, Plan/Dues columns, an Edit
action) and the dashboard dropped the second chart, so three render tests were
asserting columns and a canvas that no longer exist. Updated to the current
layout — the service-level tests were already correct, since the annotations they
check still exist even where the template stopped rendering them.
Worked the renewals KPI into the billing card: "N awaiting renewal", shown only
when non-zero. It should sit at 0 in normal running — the cron renews clubs 30
days out and they fall past the horizon — so a number here means the job has
stopped and a club is about to use the platform free, which nothing else on the
page reveals because nothing has been billed yet.
Fixed two things in the WIP table while here: a debug line that printed the raw
grace/period/owed values into the Dues cell, and a missing {% empty %} clause
(so an empty list showed a headed table with no "no clubs" row, and empty_message
was dead). Removed the stale commented-out copy of the old table.
Answers "auto-renewed but unpaid?": it is not a special case. Renewal opens an
ordinary unpaid Due, which flows unpaid -> grace -> overdue -> archive like any
period — so the safety net that the never-billed club slipped past now fires,
because there is a due to be overdue. Tested both ways.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -457,3 +457,34 @@ class RenewalTests(BillingTestBase):
|
||||
out = StringIO()
|
||||
call_command("renew_subscriptions", *args, stdout=out)
|
||||
return out.getvalue()
|
||||
|
||||
|
||||
class RenewedButUnpaidTests(BillingTestBase):
|
||||
"""A club auto-renewed that never pays the new fee flows through the ordinary
|
||||
unpaid -> grace -> overdue -> archive path. Renewal creates a normal Due; it does not
|
||||
create a special case, and the safety net that the never-billed club slipped past now
|
||||
fires, because there IS an unpaid due."""
|
||||
|
||||
def lapsed_club(self):
|
||||
"""A club on its first, PAID period — far enough back that a renewal from its end is
|
||||
itself already past grace, so only the renewal's payment state decides the outcome."""
|
||||
club = Club.objects.create(name="Renewed FC")
|
||||
subscribe(club, self.tier, start=self.today - datetime.timedelta(days=800))
|
||||
first = club.dues.first()
|
||||
record_payment(first, first.amount) # the FIRST period is settled; only the renewal is in question
|
||||
return club
|
||||
|
||||
def test_an_unpaid_renewal_becomes_overdue_and_archivable(self):
|
||||
club = self.lapsed_club()
|
||||
renewed = renew(club.subscription) # continues from the first period's end, unpaid
|
||||
|
||||
self.assertTrue(renewed.is_overdue(self.today))
|
||||
self.assertIn(renewed, dues_overdue(self.today))
|
||||
self.assertIn(club, [d.club for d in archivable_clubs(self.today)])
|
||||
|
||||
def test_a_paid_renewal_is_not_chased(self):
|
||||
club = self.lapsed_club()
|
||||
renewed = renew(club.subscription)
|
||||
record_payment(renewed, renewed.amount)
|
||||
|
||||
self.assertNotIn(club, [d.club for d in archivable_clubs(self.today)])
|
||||
|
||||
@@ -13,65 +13,98 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Club</th>
|
||||
<th></th>
|
||||
<th class="text-right">Members</th>
|
||||
<th class="text-right">Unpaid</th>
|
||||
<th class="text-right">Owed</th>
|
||||
<th>Plan</th>
|
||||
<th class="text-right">Dues</th>
|
||||
<th class="text-right">Teams</th>
|
||||
<th class="text-right">Upcoming</th>
|
||||
<th class="text-right">Admins</th>
|
||||
<th class="text-right">Teams</th>
|
||||
<th class="text-right">Events</th>
|
||||
<th class="text-right">Plan</th>
|
||||
<th class="text-right">Dues</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for club in clubs %}
|
||||
<tr>
|
||||
<td>
|
||||
<a class="link link-hover font-medium" href="{% url 'controlpanel:club_detail' club.pk %}">{{ club.name }}</a>
|
||||
<div class="mt-1 flex flex-wrap items-center gap-1">
|
||||
<span class="text-xs opacity-60">{{ club.slug }}</span>
|
||||
{% if club.is_archived %}
|
||||
{% comment %}
|
||||
An archived club's subdomain does not resolve, so "dormant" and
|
||||
"no season" would be noise: of course nothing is scheduled.
|
||||
{% endcomment %}
|
||||
<span class="badge badge-warning badge-xs gap-1">{% lucide "archive" size=10 %} Archived</span>
|
||||
<div class="flex flex-row items-center gap-4">
|
||||
<div>
|
||||
{% if club.logo %}
|
||||
<img class="h-12 w-12 object-contain" src="{{ club.logo.url }}" alt="{{ club.name }}">
|
||||
{% else %}
|
||||
{% if not club.has_season %}<span class="badge badge-warning badge-xs gap-1">{% lucide "calendar-x" size=10 %} No season</span>{% endif %}
|
||||
{% if not club.upcoming_events %}<span class="badge badge-ghost badge-xs gap-1">{% lucide "moon-star" size=10 %} Dormant</span>{% endif %}
|
||||
<div class="avatar avatar-placeholder">
|
||||
<div class="w-12 rounded-full bg-neutral text-neutral-content">
|
||||
<span>{{ club.initials }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<a class="link link-hover font-semibold tracking-wide" href="{% url "controlpanel:club_detail" club.pk %}">{{ club.name }}</a>
|
||||
<div class="text-xs opacity-60">{{ club.slug }}.rosterchief.app</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<div class="flex flex-row gap-2">
|
||||
{% if club.is_archived %}
|
||||
<span class="badge badge-warning">{% lucide "archive" size=14 %} archived</span>
|
||||
{% else %}
|
||||
{% if not club.has_season %}
|
||||
<span class="badge badge-warning">{% lucide "calendar-x" size=14 %} no seasons</span>
|
||||
{% endif %}
|
||||
|
||||
{% if not club.upcoming_events %}
|
||||
<span class="badge badge-ghost badge-outline">{% lucide "moon-star" size=14 %}dormant</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="text-right tabular-nums">{{ club.active_members }}</td>
|
||||
<td class="text-right tabular-nums {% if club.unpaid_members %}text-warning{% endif %}">{{ club.unpaid_members }}</td>
|
||||
<td class="text-right tabular-nums {% if club.outstanding %}font-semibold text-error{% endif %}">€{{ club.outstanding|floatformat:2 }}</td>
|
||||
<td>
|
||||
<td class="text-right tabular-nums">
|
||||
<div class="flex flex-row gap-2 items-center justify-end">
|
||||
{% if not club.admin_count %}
|
||||
<span class="text-error">{% lucide "triangle-alert" size=16 %}</span>
|
||||
{% endif %}
|
||||
<span class="{% if not club.admin_count %}font-bold text-error{% endif %}">{{ club.admin_count }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right tabular-nums">{{ club.team_count }}</td>
|
||||
<td class="text-right tabular-nums">{{ club.upcoming_events }}</td>
|
||||
|
||||
<td class="text-right">
|
||||
{% if club.tier_name %}
|
||||
<span class="text-sm">{{ club.tier_name }}</span>
|
||||
<span class="badge badge-accent">{{ club.tier_name|lower }}</span>
|
||||
{% else %}
|
||||
<span class="badge badge-warning badge-xs">Not billed</span>
|
||||
<span class="badge badge-ghost badge-outline">n/a</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right tabular-nums">
|
||||
|
||||
<td class="text-right">
|
||||
<div class="flex flex-row gap-2 items-center justify-end">
|
||||
{% if not club.dues_owed %}
|
||||
{% if club.tier_name %}<span class="badge badge-success badge-xs">Paid</span>{% endif %}
|
||||
{% if club.tier_name %}
|
||||
<span class="badge badge-success">paid</span>
|
||||
{% else %}
|
||||
<span class="badge badge-ghost badge-outline">n/a</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="font-semibold">€{{ club.dues_owed|floatformat:2 }}</span>
|
||||
{% if club.dues_grace_until < today %}
|
||||
<span class="badge badge-error badge-xs">Overdue</span>
|
||||
<span class="badge badge-error">overdue</span>
|
||||
{% elif club.dues_period_end < today %}
|
||||
<span class="badge badge-warning badge-xs">Grace</span>
|
||||
<span class="badge badge-warning">grace</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right tabular-nums">
|
||||
{{ club.team_count }}
|
||||
{% if club.teams_without_coach %}
|
||||
<span class="badge badge-error badge-xs ml-1" title="Teams with nobody able to pick the squad">{{ club.teams_without_coach }} no coach</span>
|
||||
{% endif %}
|
||||
|
||||
<td>
|
||||
<a class="btn btn-sm gap-2" href="{% url "controlpanel:club_detail" club.pk %}">{% lucide "pencil" size=14 %} Edit</a>
|
||||
</td>
|
||||
<td class="text-right tabular-nums">{{ club.upcoming_events }}</td>
|
||||
<td class="text-right tabular-nums {% if not club.admin_count %}text-error{% endif %}">{{ club.admin_count }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
|
||||
@@ -2,52 +2,70 @@
|
||||
{% load static lucide %}
|
||||
|
||||
{% block heading %}RosterChief Platform Dashboard{% endblock heading %}
|
||||
{% block subheading %}Welcome back {{ user.member.first_name }}!{% endblock subheading %}
|
||||
{% block subheading %}Welcome back {{ user.member.first_name }} · {% now "d b Y" %}{% endblock subheading %}
|
||||
|
||||
{% block actions %}
|
||||
<a class="btn btn-primary gap-2" href="{% url 'controlpanel:club_create' %}">{% lucide "plus" size=16 %} Create new club</a>
|
||||
{% endblock actions %}
|
||||
|
||||
{% block panel %}
|
||||
{% comment %}
|
||||
Needs attention first: these are the numbers that are supposed to be zero. A club with
|
||||
no current season cannot take a signup or schedule a match — and it fails silently,
|
||||
nothing errors — while an admin without a second factor is locked out of their own
|
||||
club. Both are work queues, not statistics. The vanity totals sit further down.
|
||||
{% endcomment %}
|
||||
<div class="mb-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div class="card bg-base-100 shadow {% if attention.clubs_without_season %}border-l-4 border-warning{% endif %}">
|
||||
<div class="mb-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-6">
|
||||
<div class="card bg-base-100 shadow border-l-4 border-info">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center gap-2 text-sm opacity-70">{% lucide "calendar-x" size=16 %} No current season</div>
|
||||
<div class="text-3xl font-bold tabular-nums">{{ attention.clubs_without_season }}</div>
|
||||
<div class="flex items-center gap-2 text-sm opacity-70 mb-3">{% lucide "building-2" size=16 %} Clubs</div>
|
||||
<div class="text-4xl font-bold tabular-nums font-mono">{{ totals.clubs }}</div>
|
||||
<div class="text-xs opacity-60">Managing {{ totals.members }} member{{ totals.members|pluralize }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 shadow border-l-4 border-info">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center gap-2 text-sm opacity-70 mb-3">{% lucide "archive" size=16 %} Archived clubs</div>
|
||||
<div class="text-4xl font-bold tabular-nums font-mono">{{ totals.archived_clubs }}</div>
|
||||
<div class="text-xs opacity-60">Not accessible but data maintained</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 shadow border-l-4 border-success {% if attention.clubs_without_season %}border-warning{% endif %}">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center gap-2 text-sm opacity-70 mb-3">{% lucide "calendar-x" size=16 %} No current season</div>
|
||||
<div class="text-4xl font-bold tabular-nums font-mono">{{ attention.clubs_without_season }}</div>
|
||||
<div class="text-xs opacity-60">Clubs that cannot take signups</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card bg-base-100 shadow {% if attention.dormant_clubs %}border-l-4 border-warning{% endif %}">
|
||||
|
||||
<div class="card bg-base-100 shadow border-l-4 border-success {% if attention.dormant_clubs %}border-warning{% endif %}">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center gap-2 text-sm opacity-70">{% lucide "moon-star" size=16 %} Dormant</div>
|
||||
<div class="text-3xl font-bold tabular-nums">{{ attention.dormant_clubs }}</div>
|
||||
<div class="text-xs opacity-60">Nothing scheduled in 30 days</div>
|
||||
<div class="flex items-center gap-2 text-sm opacity-70 mb-3">{% lucide "moon-star" size=16 %} Dormant clubs</div>
|
||||
<div class="text-4xl font-bold tabular-nums font-mono">{{ attention.dormant_clubs }}</div>
|
||||
<div class="text-xs opacity-60">No events scheduled next 30 days</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card bg-base-100 shadow {% if attention.admins_pending_mfa %}border-l-4 border-error{% endif %}">
|
||||
|
||||
<div class="card bg-base-100 shadow border-l-4 border-success {% if attention.admins_pending_mfa %}border-warning{% endif %}">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center gap-2 text-sm opacity-70">{% lucide "shield-alert" size=16 %} MFA pending</div>
|
||||
<div class="text-3xl font-bold tabular-nums">{{ attention.admins_pending_mfa }}</div>
|
||||
<div class="text-xs opacity-60">Admins locked out until they enrol</div>
|
||||
<div class="flex items-center gap-2 text-sm opacity-70 mb-3">{% lucide "shield-alert" size=16 %} MFA pending</div>
|
||||
<div class="text-4xl font-bold tabular-nums font-mono">{{ attention.admins_pending_mfa }}</div>
|
||||
<div class="text-xs opacity-60">Admins without MFA configured</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card bg-base-100 shadow {% if attention.dues_owed %}border-l-4 border-error{% endif %}">
|
||||
|
||||
<div class="card bg-base-100 shadow border-l-4 border-success {% if attention.dues_owed %}border-warning{% endif %}">
|
||||
<div class="card-body p-4">
|
||||
{% comment %}
|
||||
What the CLUBS owe US. Not to be confused with the club-shop money below,
|
||||
which members owe their clubs and is never ours.
|
||||
{% endcomment %}
|
||||
<div class="flex items-center gap-2 text-sm opacity-70">{% lucide "receipt-euro" size=16 %} Dues owed</div>
|
||||
<div class="text-3xl font-bold tabular-nums">€{{ attention.dues_owed|floatformat:2 }}</div>
|
||||
<div class="flex items-center gap-2 text-sm opacity-70 mb-3">{% lucide "receipt-euro" size=16 %} Payment pending</div>
|
||||
<div class="text-4xl font-bold tabular-nums font-mono">€{{ attention.dues_owed|floatformat:2 }}</div>
|
||||
<div class="text-xs opacity-60">
|
||||
{{ attention.dues_in_grace }} in grace ·
|
||||
<span class="{% if attention.dues_overdue %}font-semibold text-error{% endif %}">{{ attention.dues_overdue }} overdue</span>
|
||||
{% comment %}
|
||||
Renewals pending should sit at ~0: the cron job renews clubs 30 days out and
|
||||
then they fall past the horizon. A number that lingers here means the job has
|
||||
stopped and a club is about to use the platform for free — which no other
|
||||
figure on this page reveals, because nothing has been billed yet.
|
||||
{% endcomment %}
|
||||
{% if attention.renewals_pending %}
|
||||
· <span class="font-semibold text-warning">{{ attention.renewals_pending }} awaiting renewal</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,22 +81,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-base">{% lucide "receipt-euro" size=18 %} Platform dues per month</h2>
|
||||
<p class="text-sm opacity-70">What clubs paid us. Club-shop money is theirs, not ours.</p>
|
||||
<div class="h-56">
|
||||
<canvas id="revenue-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6 grid gap-4 lg:grid-cols-2">
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-base">{% lucide "milestone" size=18 %} Onboarding</h2>
|
||||
<p class="text-sm opacity-70">Where clubs stall. One with no team or no events is a shell.</p>
|
||||
<p class="text-sm opacity-70">Tracking club onboarding to ensure a smooth start</p>
|
||||
<div class="mt-2 space-y-3">
|
||||
{% for step in funnel %}
|
||||
<div>
|
||||
@@ -86,76 +93,18 @@
|
||||
<span class="flex items-center gap-2">{% lucide step.icon size=14 %} {{ step.label }}</span>
|
||||
<span class="font-semibold tabular-nums">{{ step.count }}</span>
|
||||
</div>
|
||||
<progress class="progress progress-primary w-full" value="{{ step.count }}" max="{{ funnel.0.count }}"></progress>
|
||||
<progress class="progress {% if step.count == funnel.0.count %}progress-success{% elif step.count == 0 %}progress-error{% else %}progress-warning{% endif %} w-full" value="{{ step.count }}"
|
||||
max="{{ funnel.0.count }}"></progress>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="card-title text-base">{% lucide "toggle-right" size=18 %} Feature adoption</h2>
|
||||
<a class="btn btn-ghost btn-xs" href="{% url 'controlpanel:features' %}">Manage</a>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-sm">
|
||||
<tbody>
|
||||
{% for flag in flags %}
|
||||
<tr>
|
||||
<td class="font-mono font-medium">{{ flag.name }}</td>
|
||||
<td class="text-right">
|
||||
{% if flag.overridden %}
|
||||
{# `everyone` overrides club targeting, so the club count says nothing here. #}
|
||||
<span class="badge badge-sm {% if flag.everyone %}badge-success{% else %}badge-error{% endif %}">
|
||||
{% if flag.everyone %}On for all{% else %}Off everywhere{% endif %}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="tabular-nums">{{ flag.clubs }} / {{ totals.clubs }} clubs</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td class="text-center opacity-60">No features yet.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats mb-6 w-full bg-base-100 shadow">
|
||||
<div class="stat">
|
||||
<div class="stat-title">Active clubs</div>
|
||||
<div class="stat-value">{{ totals.clubs }}</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Archived</div>
|
||||
<div class="stat-value">{{ totals.archived_clubs }}</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Members</div>
|
||||
<div class="stat-value">{{ totals.members }}</div>
|
||||
<div class="stat-desc">{{ attention.members_without_login }} without a login</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Club admins</div>
|
||||
<div class="stat-value">{{ totals.admins }}</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Not billed</div>
|
||||
<div class="stat-value {% if attention.clubs_unbilled %}text-warning{% endif %}">{{ attention.clubs_unbilled }}</div>
|
||||
<div class="stat-desc">Clubs on no tier</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Clubs</h2>
|
||||
<h2 class="card-title">{% lucide "building-2" size=18 %} Clubs</h2>
|
||||
{% include "controlpanel/_club_health_table.html" %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -219,22 +168,22 @@
|
||||
data: {
|
||||
labels: data.signups.map((point) => point.month),
|
||||
datasets: [
|
||||
{ label: "New", data: data.signups.map((point) => point.new), backgroundColor: css("--color-primary", "#4f46e5") },
|
||||
{ label: "Returning", data: data.signups.map((point) => point.returning), backgroundColor: css("--color-accent", "#0ea5e9") },
|
||||
{label: "New", data: data.signups.map((point) => point.new), backgroundColor: css("--color-primary", "#4f46e5")},
|
||||
{label: "Returning", data: data.signups.map((point) => point.returning), backgroundColor: css("--color-accent", "#0ea5e9")},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: { legend: { position: "bottom", labels: { color: ink } } },
|
||||
plugins: {legend: {position: "bottom", labels: {color: ink}}},
|
||||
scales: {
|
||||
x: { stacked: true, ticks: { color: ink }, grid: { color: grid } },
|
||||
y: { stacked: true, beginAtZero: true, ticks: { color: ink, precision: 0 }, grid: { color: grid } },
|
||||
x: {stacked: true, ticks: {color: ink}, grid: {color: grid}},
|
||||
y: {stacked: true, beginAtZero: true, ticks: {color: ink, precision: 0}, grid: {color: grid}},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return [signups, build("revenue-chart", "Dues", data.dues, css("--color-accent", "#0ea5e9"), "bar", true)];
|
||||
return [signups]; // build("revenue-chart", "Dues", data.dues, css("--color-accent", "#0ea5e9"), "bar", true)];
|
||||
};
|
||||
|
||||
let charts = render();
|
||||
|
||||
@@ -751,8 +751,8 @@ class DashboardMetricsTests(ControlPanelTestBase):
|
||||
|
||||
self.assertContains(response, "No current season")
|
||||
self.assertContains(response, "MFA pending")
|
||||
self.assertContains(response, "Payment pending")
|
||||
self.assertContains(response, 'id="signups-chart"')
|
||||
self.assertContains(response, 'id="revenue-chart"')
|
||||
self.assertContains(response, "js/chart.js")
|
||||
self.assertIn("signups", response.context["charts"])
|
||||
|
||||
@@ -1014,18 +1014,17 @@ class ClubHealthTableTests(TestCase):
|
||||
|
||||
response = self.client.get(reverse("controlpanel:dashboard"))
|
||||
|
||||
self.assertContains(response, "Owed")
|
||||
self.assertContains(response, "Upcoming")
|
||||
self.assertContains(response, "Unpaid")
|
||||
# Health, not vanity: Plan and Dues each name something to act on, next to the counts.
|
||||
for column in ("Members", "Admins", "Teams", "Events", "Plan", "Dues"):
|
||||
self.assertContains(response, f">{column}</th>")
|
||||
|
||||
|
||||
class ClubListHealthTests(ControlPanelTestBase):
|
||||
def test_the_list_shows_the_same_health_columns_as_the_dashboard(self):
|
||||
response = self.client.get(reverse("controlpanel:club_list"))
|
||||
|
||||
self.assertContains(response, "Owed")
|
||||
self.assertContains(response, "Upcoming")
|
||||
self.assertContains(response, "Unpaid")
|
||||
for column in ("Members", "Admins", "Teams", "Events", "Plan", "Dues"):
|
||||
self.assertContains(response, f">{column}</th>")
|
||||
self.assertTemplateUsed(response, "controlpanel/_club_health_table.html")
|
||||
|
||||
def test_an_archived_club_is_badged_archived_rather_than_dormant(self):
|
||||
@@ -1097,6 +1096,24 @@ class PlatformDuesMetricTests(TestCase):
|
||||
|
||||
self.assertEqual(platform_attention()["clubs_unbilled"], 0)
|
||||
|
||||
def test_renewals_pending_counts_clubs_about_to_lapse(self):
|
||||
# ~0 in normal running; a number here means the renewal cron has stopped.
|
||||
self.assertEqual(platform_attention()["renewals_pending"], 0)
|
||||
|
||||
subscribe(self.club, self.tier, start=self.today - datetime.timedelta(days=350)) # ends in 15 days
|
||||
|
||||
self.assertEqual(platform_attention()["renewals_pending"], 1)
|
||||
|
||||
def test_the_dashboard_surfaces_pending_renewals(self):
|
||||
# The whole point of the KPI: a club about to go free is visible, though nothing is
|
||||
# owed yet, so no other figure on the page would show it.
|
||||
subscribe(self.club, self.tier, start=self.today - datetime.timedelta(days=350))
|
||||
staff = User.objects.create_user(email="staff@example.com", password="pw-secret-123", is_staff=True)
|
||||
enrol_mfa(staff)
|
||||
self.client.force_login(staff)
|
||||
|
||||
self.assertContains(self.client.get(reverse("controlpanel:dashboard")), "awaiting renewal")
|
||||
|
||||
def test_platform_dues_and_club_shop_money_are_different_charts(self):
|
||||
subscribe(self.club, self.tier)
|
||||
record_payment(self.club.dues.first(), Decimal("500.00"))
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user