Skip to content

[WIP] feat(admin): Leaderboard — top earners per month + MSO comp - #145

Draft
hhff wants to merge 12 commits into
mainfrom
feat/leaderboard-top-earners
Draft

[WIP] feat(admin): Leaderboard — top earners per month + MSO comp#145
hhff wants to merge 12 commits into
mainfrom
feat/leaderboard-top-earners

Conversation

@hhff

@hhff hhff commented Jul 20, 2026

Copy link
Copy Markdown
Member

Status: WIP / draft. Data model and math are verified against production; the MSO tier structure (labels + multipliers) is still being tuned, so treat the specific figures as illustrative, not final.

What

Adds a Leaderboard page under the Money tab (admins only): the top X earners for each month, styled as a native ActiveAdmin index. Above each month's table sits an MSO Compensation panel that turns that month's average top-X earner into what each leadership tier would be paid.

How earnings are defined

  • Aggregate ContributorPayout amounts across all of a contributor's ledgers. A contributor has one Ledger per Enterprise, so payouts are grouped by ledgers.contributor_id.
  • Months bucket on invoice_passes.start_of_month, matching the canonical dating used by ContributorPayout#effective_on_for_display (a payout has no date column of its own).
  • Trueups are excluded. A Trueup is what brings a leadership role up to the benchmark below — counting it as earnings would feed the benchmark back into itself and inflate it every month. Trueups live in their own table (not a type column), so the exclusion is structural: we simply never sum them.
  • ContributorPayout is acts_as_paranoid, so soft-deleted rows are already excluded by its default scope.

Verified against restored production data: April 2026 reproduces the finance team's independently-maintained figures to the cent (james $28,720.96, owen $25,537.20, andy $23,569.60, zachary $20,577.38).

MSO Compensation

Each tier is paid that month's average top-X earner × the tier multiplier. The ladder steps by a uniform +0.15 per rung:

Tier Multiplier
Heads 0.70x
SVPs 0.85x
Chiefs 1.00x
Principals 1.15x

The tiers live in Stacks::Leaderboard::MSO_TIERS (with a MSO_TIER_STEP constant), single-sourced so the view, labels, and math all follow one definition — renaming a tier or changing a multiplier is a one-line edit. A test asserts the ladder stays evenly stepped, so a future change that breaks the +0.15 spacing fails loudly.

This is the other half of the Trueup exclusion: the Trueup is the payment that lifts one of these roles up to the figure computed here.

Behaviour

  • X defaults to 5; ?limit=N overrides it via pill toggles (3 / 4 / 5 / 10 / 20), clamped to 1..50 so a hostile or fat-fingered param can't blow up the page. Top-4 matches the basis of the original leadership-comp spreadsheet.
  • Right-hand pill shows the average of the listed earners (the benchmark), not the total; the total moves to a table footer.
  • Contributor names link through to their contributor page.
  • Months render most-recent-first; contributors with no positive earnings are omitted; display name prefers full name, falling back to email.

Styling

Matches the Money (payable QBO bills) screen so the two read as one system — .title_bar headers, .index_table.index tables, .pill.complete badges, and the standard #index_footer > .download_links download row. The MSO panel reuses ActiveAdmin's div.panel > h3 + div.panel_contents markup (hand-written, since the Arbre panel builder isn't available in an ERB partial).

CSV export — the ActiveAdmin way

The index responds to .csv, so the download lives at the conventional /admin/leaderboard.csv?limit=N with the standard "Download: CSV" footer link. (AA's csv do…end DSL is ResourceDSL-only and needs an AR collection, so a register_page can't use it directly — this reproduces the same convention by hand.) Columns: Month, Rank, Contributor, Earnings.

Access control

Nested under Money and hidden from the nav for non-admins. Menu visibility is presentation, not access control, so a before_action :require_admin! is the real gate across every format — a non-admin can't pull the CSV by hitting the URL. (AA's own authorize_access! runs first and answers a non-HTML format with 401 rather than a redirect; the CSV test asserts the security property — never 200, no rows emitted — rather than a specific status.)

Verification

  • test/lib/stacks/leaderboard_test.rb — ranking/limit, Trueup exclusion, cross-ledger aggregation, zero-earning omission, name formatting, sanitize_limit clamping, CSV (incl. comma-escaping), MSO tier math + even-ladder invariant.
  • test/integration/admin_leaderboard_test.rb — renders, ?limit= toggles, out-of-range/junk limits, contributor links, MSO panel, .csv download, non-admin block.
  • Full test/integration + test/lib/stacks suites: 267 runs, 787 assertions, 0 failures.
  • Driven end-to-end in a local dev server against restored production data.

Not in this PR

The Google-Drive explainer spreadsheet (MSO-Compensation-Model.xlsx) is a separate deliverable, not code.

🤖 Generated with Claude Code

hhff and others added 12 commits July 20, 2026 01:10
Adds an admin-only ActiveAdmin page listing the top X earners for each
month, styled as a ledger.

Earnings are the aggregate of a contributor's ContributorPayouts across
all of their ledgers (a contributor has one ledger per enterprise),
bucketed on invoice_passes.start_of_month to match the canonical dating
used by ContributorPayout#effective_on_for_display.

Trueups are excluded. A Trueup is what tops a leadership role up to the
average of the top earners, so counting it would let the roles being
topped up appear at the top of the board they are benchmarked against.
Because Trueups live in their own table rather than as a type column on
a shared ledger table, the exclusion is structural: we never sum them.

X defaults to 5 and is overridable via ?limit=, clamped to 1..50 so a
hostile or fat-fingered param cannot blow up the page.

The global AuthorizationAdapter admits project leads to most pages, so
the page additionally gates on current_admin_user.is_admin? in a
before_action, and hides itself from the menu for non-admins.

Verified against the restored production dataset: April 2026 reproduces
the finance team's independently-maintained figures to the cent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nthly average

Swaps the hand-rolled inline "ledger paper" styling for the same markup the
Money (payable QBO bills) screen uses, so the two read as one system: a
.title_bar per group with the heading on the left and a .pill.complete on the
right, over a standard .index_table.index table with even/odd rows and
.text-right amounts. The Arbre block becomes an ERB partial, matching how the
other content-heavy register_pages (e.g. OKR Explorer) render.

The right-hand pill now shows the AVERAGE earned across the listed top-N
contributors rather than the month's total — this is the "average top-N earner"
figure the leadership compensation model multiplies against. The total moves to
a footer row on the table, so no information is lost.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…header copy

- Each contributor name now links through to their contributor page via
  link_to admin_contributor_path, matching the Money screen. Entry already
  carries contributor_id, so this costs no extra queries.
- The limit toggles become the same nag-pill tab bar the Money page uses,
  with .complete marking the active limit.
- Removes the explanatory paragraph above the table. The reasoning for
  excluding Trueups lives in the code comments and the PR, not the chrome.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds an "Export CSV" action to the Leaderboard, rendered top-right in a
.title_bar the way the Money page renders "Sync all to QBO". The export is a
page_action, so it runs through the same require_admin! before_action as the
page itself — a non-admin cannot pull the data by hitting the URL directly.

Stacks::Leaderboard.to_csv emits one row per ranked contributor with the
month's average and total repeated on each row, so the file pivots cleanly.
Amounts are plain decimals rather than currency strings so spreadsheets read
them as numbers. The export honors ?limit=, and the filename records both the
limit and the export date.

The page now sits under the Money tab and is hidden from the nav for anyone
who isn't an admin. Menu visibility is presentation, not access control, so
the before_action remains the actual gate; the menu proc uses &. so it stays
falsy rather than raising for a logged-out visitor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ActiveAdmin's `csv do ... end` DSL lives on ResourceDSL and needs an
ActiveRecord-backed collection, so a register_page can't use it (PageDSL
exposes only content/page_action/belongs_to). We get the same convention by
hand instead of inventing our own:

- The index action now responds to .csv, so the download lives at the
  conventional /admin/leaderboard.csv?limit=N instead of a bespoke
  /admin/leaderboard/export_csv page_action.
- The bespoke "Export CSV" action_item is replaced by the standard
  #index_footer > .download_links markup ActiveAdmin renders under an index
  table ("Download: CSV"), so it inherits existing styling and sits where
  admins already look for it.

Access is unchanged in substance: require_admin! still guards every format.
Note that AA's own authorize_access! runs first and answers a non-HTML format
with 401 rather than a redirect, so the CSV test asserts the security property
(never 200, no rows emitted) rather than a specific status code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both are derivable from the exported rows, so repeating them on every line
was denormalised noise in a spreadsheet. The CSV is now Month, Rank,
Contributor, Earnings. The on-screen pill and table footer still show the
average and total, where they're the point.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Placeholder section (body is TKTKTK) sitting between each month's heading
and its table. Uses the same markup ActiveAdmin's Arbre `panel` builder
emits — div.panel > h3 + div.panel_contents — so it inherits the standard
panel styling from an ERB template, where the Arbre builder isn't available.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fills the MSO Compensation panel with the three leadership tiers, each
paying the month's average top-N earner times its multiplier:
Heads 0.8x, Chiefs 1x, Founders 1.2x.

Tiers live in Stacks::Leaderboard::MSO_TIERS with a mso_compensation helper
rather than being hardcoded in the view, so the multipliers are single-sourced
and unit-testable. The multiplier label strips a trailing .0 so Chiefs reads
"1x" rather than "1.0x".

This is the other half of the Trueup exclusion: a Trueup is the payment that
lifts one of these roles up to the figure computed here, so counting Trueups
as earnings would feed the benchmark back into itself.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SVPs sit exactly halfway between Heads and Chiefs — (0.7 + 1.0) / 2 — which
a test now asserts against the constant rather than the literal, so the
midpoint relationship survives any future change to either neighbour.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Renames the top MSO tier and moves it to 1.15x so the ladder steps evenly:
0.7 / 0.85 / 1.0 / 1.15, a uniform +0.15 between neighbours. The step is
named (MSO_TIER_STEP) and asserted across the whole ladder, so a future
change to any single tier that breaks the even spacing fails loudly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a top-4 view alongside 3/5/10/20. Top-4 is the basis the original
leadership compensation spreadsheet used, so this makes the page directly
comparable to it without hand-editing the query string.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@hhff
hhff marked this pull request as draft July 28, 2026 20:14
@hhff hhff changed the title feat(admin): Leaderboard — top earners per month [WIP] feat(admin): Leaderboard — top earners per month + MSO comp Jul 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant