A Billing tab (tiers, their dated prices, and everything we are owed), a billing
panel on each club (plan, periods, payment history, invoice), and the dues on the
dashboard and the club tables.
Every state change goes through the billing service, and a BillingError surfaces
as a message rather than a 500 -- so "that period is waived", "no price in force",
"already billed for that period" and a missing PDF library all explain themselves
instead of crashing.
The dashboard now separates the two pots of money that were previously one word.
"Revenue per month" was CLUB SHOP revenue -- members paying their clubs, which is
never ours -- sitting on our dashboard under a label that implied it was income.
It is now "Platform dues per month" (what clubs paid us) with the club-shop series
renamed club_revenue, and the club tables carry a Plan column and what each club
owes us, annotated in the same single query.
Rate changes are add-only in the UI as well as the model: the price form creates a
dated row and never edits the last one, and a test asserts that raising the rate
leaves an already-open period at the amount it was billed at.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>