A Platform Admin can see revenue and enrolment for the whole platform over a date range: per course, per Instructor, and month by month. Today the only analytics page is the Instructor's own (/instructor/analytics), which shows one Instructor their own courses. Nobody can see the platform as a whole.
Everything here reads the tables that already exist: purchases, refunds, courses and enrollments. No schema changes.
A new page, /admin/analytics, next to the other admin pages (admin.courses, admin.refunds, …).
Date range. A from/to range at the top, defaulting to the last 12 months up to today. It lives in the URL (?from=YYYY-MM-DD&to=YYYY-MM-DD), so a report can be linked. An invalid or reversed range falls back to the default instead of throwing.
Platform totals for the range:
- Gross revenue: the sum of
purchases.amount_paidfor Purchases made in the range. - Refunded: the sum of
refunds.amount_refundedon Approved Refund Requests against those Purchases. - Net revenue: gross minus refunded.
- Purchases: how many Purchases were made in the range.
- New Enrollments: how many Enrollments started (
enrollments.enrolled_at) in the range.
Per course: one row per course with at least one Purchase or Enrollment in the range. It shows the course, its Instructor, Purchases, gross, refunded, net and new Enrollments. Sorted by net revenue, highest first.
Per Instructor: the same figures rolled up across each Instructor's courses, one row per Instructor. Sorted by net revenue, highest first.
Monthly trend: one row per calendar month (UTC) in the range, showing gross, refunded, net, Purchases and new Enrollments. Months with no activity still appear, with zeros.
- Refunds count against the Purchase they refund. An Approved Refund Request is subtracted in the period, course and Instructor of its Purchase, not in the month it was decided. Open and Declined Refund Requests subtract nothing.
- Money is in the same integer unit as
purchases.amount_paidand is shown with the app's existing price formatting. - Dates are UTC.
fromandtoare both inclusive whole days. - Platform Admins only. Anyone else, including an Instructor, gets the same response the other admin pages give a non-admin. Use the existing permissions module; don't write a new check.
- A Platform Admin can open
/admin/analyticsand see the totals, per-course table, per-Instructor table and monthly trend for the default range. - Changing the range changes every figure, and the range survives a reload.
- With one Purchase and one Approved Refund Request for the full amount, net revenue for that course, its Instructor, its month and the platform total is zero, and gross shows the Purchase.
- A Declined or Open Refund Request leaves net revenue unchanged.
- A month with no Purchases and no Enrollments shows as a row of zeros.
- A Student or an Instructor who visits
/admin/analyticsis refused. - The figures come from a module under
app/modules/with an entry point the tests call directly against a test database, asinstructor-analyticsdoes. The loader only calls it.
purchases.created_at and courses.instructor_id have no index. Every date-ranged query here is a full scan of purchases, and every per-Instructor rollup scans courses. That is intended. Do not add either index, or any other index or migration, as part of this spec. Something downstream is meant to find the slow queries for itself.
- Any schema change, index or migration.
- Charts. Tables are enough for this spec.
- CSV export.
- Filtering by category, country or Purchasing-Power Price.
- Splitting Individual Purchases from Team Licences.
- Changing the Instructor's own analytics page.