Skip to content

D572 part 3b (#1183): payout destination (Q1 = A) and the leaderboard by metric, period and change (migration 409) - #1324

Merged
guillaumelauzier merged 1 commit into
mainfrom
agent/claude-b/1183-earnings-part3b
Oct 6, 2026
Merged

guillaumelauzier merged 1 commit into
mainfrom
agent/claude-b/1183-earnings-part3b

Conversation

@guillaumelauzier

Copy link
Copy Markdown
Contributor

Objective

Part 3b of D572. It does two things:

  • Records where a partner's payout is sent: a label and the last 4 characters (Q1 = A).
  • Ranks the leaderboard by metric and period, with the change against an earlier standing. Before this, the page showed those as stated absences.

Closes #1183

Implementation

  • Migration 409. Adds payouts.destination_label and payouts.destination_last4. Adds the partner_leaderboard_snapshots table (primary key: date, metric, period key, user), with an index for lookups.
  • POST /network/payouts takes an optional destination.
    • It is both fields or neither. The label is at most 60 characters with no control characters. The last 4 are alphanumeric and stored upper-cased.
    • A bad destination is refused (destination_incomplete, invalid_destination_label, invalid_destination_last4) before any commission is claimed.
    • The runtime CREATE TABLE payouts gains both columns, to keep schema-pair parity.
  • services/partnerLeaderboard.ts (new) ranks three metrics:
    • score: the network score. It is a standing, so all time only.
    • referrals: referrals made, as a count.
    • commissions: commissions earned, as a count, with reversed ones left out. It is a count and not a sum because commissions are held in more than one currency, and a sum would publish what each member earned.
    • Periods are all time or the current UTC calendar quarter.
    • Members who opted out of the directory are not ranked.
    • Equal values share a rank and the next rank skips.
  • The change compares with the most recent snapshot at least 7 days old.
    • A member who was not ranked then reads "new".
    • With no snapshot that old, the change is withheld and the reason is given.
    • A quarter's board compares only within its own quarter.
  • Snapshots are written daily by the cron at 05:25 UTC, using INSERT OR IGNORE (idempotent). Each board is written on its own, so one failure costs only that board's rows.
  • GET /partnernet/leaderboard/board?metric&period returns the top 25 rows.
    • Each row has rank, role, value, change and is_viewer. No id, name or email is sent for any other member.
    • It also returns the viewer's own standing, or the reason they are not ranked (hidden, or nothing counted), plus compare and total_ranked.
    • Refusals: bad_metric, bad_period, period_not_for_metric, and 503 board_unreadable.
  • Frontend.
    • Partner earnings: the form gains "Sent to" inputs and a confirm row. The history shows the destination or "destination not named".
    • Admin queue: shows the destination.
    • Leaderboard page: metric and period chips (Introductions is drawn disabled, with its reason), a change column, and the viewer's standing.
  • For the reviewer: the all-time lower bound is '1970-01-01 00:00:00', not 0000-…, because SQLite's datetime() reads the latter as NULL. This is noted in D572.

Files changed

  • cloudflare-worker/sql/migrations/409_payout_destination_and_leaderboard.sql: new columns and the snapshot table.
  • cloudflare-worker/src/services/partnerLeaderboard.ts: ranking, snapshots, comparison.
  • cloudflare-worker/src/routes/partnernet.ts: GET /leaderboard/board.
  • cloudflare-worker/src/routes/network.ts: readPayoutDestination, the insert, and the runtime CREATE.
  • cloudflare-worker/src/index.ts: the snapshot cron.
  • frontend/src/lib/api.js: partnerLeaderboardBoard replaces partnerLeaderboard.
  • frontend/src/lib/partnerEarnings.js: destination helpers, board metrics and periods, changeText.
  • frontend/src/pages/partner/earnings/PartnerEarnings.jsx, PartnerLeaderboard.jsx, frontend/src/pages/admin/AdminCommissions.jsx: the UI changes above.
  • documentation/architecture/decisions/D572.md: the Part 3b section.
  • Tests: cloudflare-worker/test/partner_earnings_3b_d572.test.ts (new), frontend/test/partner_leaderboard_d572.test.mjs (new), and updates to partner_earnings_d569.test.mjs and partner_earnings_d572.test.ts.

Testing

  • npm run build: exit 0.
  • npm run test:drift > drift.log 2>&1; echo EXIT=$?: EXIT=0.
  • The worker 3b test (11 tests) runs on real SQLite with the migrations applied. All 20 mutations were caught.
  • The frontend 3b test (8 tests): every mutation was caught.
  • docs/ is not committed.

Risks

  • Ships to production: the Worker, the frontend build, and D1 migration 409, which is additive only (two nullable columns and a new table).
  • Rollback: revert the PR. The columns and table can stay.
  • Cron failure: if the snapshot write fails, the board still serves and the change is withheld with its reason.
  • Security: no secret in the diff, and no dangerouslySetInnerHTML. Auth gates are unchanged; the board route sits behind the existing partnernet auth. Other members appear only as rank and role. Only the last 4 characters of a destination are stored.

Dependencies

  • Builds on the D572 parts 1–3a, already merged.
  • Migration number 409 was assigned by S1.

Agent

S09 · Claude Code

Review requested

S1

🤖 Generated with Claude Code

https://claude.ai/code/session_01L8iwbvTs6nntzGe7MxmvUm


Generated by Claude Code

…eriod and change

- Migration 409: payouts.destination_label / destination_last4, and
  partner_leaderboard_snapshots written daily by the cron at 05:25 UTC.
- POST /network/payouts takes an optional destination (label and last 4,
  both or neither) and refuses a bad one before any commission is claimed.
- GET /partnernet/leaderboard/board ranks by score, referrals or
  commissions (counts, never sums), all time or this quarter, with the
  change against a standing at least 7 days old, or the reason there is none.
- Partner earnings form, history and admin queue show where a payout is sent;
  the leaderboard page gains metric and period chips and a change column.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L8iwbvTs6nntzGe7MxmvUm
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown

Preview: https://studioos-pr-1324.guillaumelauzier.workers.dev (built from aeb7b56)

The pull request's SPA build on a Worker with no bindings: pages and deep links work, /api/* is a 404, and nothing here can reach production data. Redeployed on every push; deleted when the PR closes.

@guillaumelauzier
guillaumelauzier marked this pull request as ready for review October 6, 2026 23:19
@guillaumelauzier
guillaumelauzier merged commit 8560c69 into main Oct 6, 2026
21 checks passed
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.

Partner earnings backend: failed payouts reopen their commissions, plus the fields the screens show as Not recorded (D572, migration 393)

2 participants