Skip to content

Fix bugs, DRY up code, add monthly/daily summaries, and optimize dashboard (~176 → 38 queries)#8

Merged
rameerez merged 1 commit intomainfrom
fix-bugs-dry-optimize-dashboard
Feb 10, 2026
Merged

Fix bugs, DRY up code, add monthly/daily summaries, and optimize dashboard (~176 → 38 queries)#8
rameerez merged 1 commit intomainfrom
fix-bugs-dry-optimize-dashboard

Conversation

@rameerez
Copy link
Owner

@rameerez rameerez commented Feb 9, 2026

Summary

Full review of the profitable gem against the Pay gem v11 source and LicenseSeat production schema. Fixes bugs, consolidates duplicated logic, and reduces dashboard query count from ~176 to 38.

Bug Fixes

  • calculate_new_mrr_in_period too permissive: Was using where.not(status: EXCLUDED_STATUSES + CHURNED_STATUSES) which let through incomplete, incomplete_expired, and unpaid subscriptions. Now uses where(status: 'active') to match MrrCalculator's logic — only active subscriptions contribute MRR.
  • calculate_new_subscribers inconsistent filtering: The public method wasn't filtering excluded statuses (trialing, paused), but the _in_period version was. Now delegates to calculate_new_subscribers_in_period for consistent behavior.
  • Constants inaccessible from MrrCalculator: EXCLUDED_STATUSES and CHURNED_STATUSES were defined inside class << self, making them unreachable as Profitable::EXCLUDED_STATUSES. Moved to module level.
  • MrrCalculator hardcoded status list: Replaced ['trialing', 'paused'] with the shared Profitable::EXCLUDED_STATUSES constant.

DRY Consolidation

Five period-based methods now delegate to their _in_period counterparts instead of duplicating the query logic:

  • calculate_churncalculate_churn_rate_for_period
  • calculate_churned_customerscalculate_churned_subscribers_in_period
  • calculate_churned_mrrcalculate_churned_mrr_in_period
  • calculate_new_mrrcalculate_new_mrr_in_period
  • calculate_new_subscriberscalculate_new_subscribers_in_period

Added subscriptions_with_processor helper to eliminate repeated .includes(:customer).select('...customer_processor').joins(:customer) chains.

Dashboard Performance (~176 → 38 queries)

Section Before After
Monthly summary (12 months) 72 queries (6/month × 12) 5 queries (bulk load + Ruby grouping)
Daily summary (30 days) 60 queries (2/day × 30) 2 queries (bulk load + Ruby grouping)
Period cards (3 periods) 27 queries (9/period × 3) 18 queries (6/period via period_data)
Top cards + header 17 queries 13 queries (+ 2 cached by Rails)
Total ~176 38 (+ 2 cached)

How:

  • Batched monthly_summary: 5 bulk queries load all subscription data for the full 12-month range, then groups by month in Ruby. No DB-specific SQL needed.
  • Batched daily_summary: Same pattern — 2 bulk queries for the full 30-day range.
  • New period_data method: Computes all 7 period metrics in one pass, reusing intermediate values (e.g., churned_count used for both the churned customers display and churn rate calculation; new_mrr/churned_mrr used for display and mrr_growth).
  • Controller precomputation: Moved all Profitable.* calls from the view into the controller as instance variables.

Verified on LicenseSeat production:

Completed 200 OK in 71ms (Views: 2.1ms | ActiveRecord: 23.4ms (38 queries, 2 cached))

New Public API

  • Profitable.period_data(in_the_last:) — returns a hash of NumericResult values: new_customers, churned_customers, churn, new_mrr, churned_mrr, mrr_growth, revenue

Test plan

  • bundle exec rake test — 249 tests, 382 assertions, 0 failures, 0 errors
  • Coverage: 94.64% line, 90.77% branch
  • Production sync verified: diff between lib/profitable.rb and test/test_helper.rb shows only a comment block difference
  • Tested on LicenseSeat with real Stripe data — 38 queries, 71ms

🤖 Generated with Claude Code

…queries)

Bug fixes:
- Fix calculate_new_mrr_in_period using overly permissive status filter
  (where.not excluded+churned let through incomplete/unpaid; now uses
  where(status: 'active') to match MrrCalculator logic)
- Fix calculate_new_subscribers inconsistency (wasn't filtering excluded
  statuses; now delegates to calculate_new_subscribers_in_period)
- Move EXCLUDED_STATUSES/CHURNED_STATUSES to module level so
  MrrCalculator can reference them as Profitable::EXCLUDED_STATUSES
- Use shared Profitable::EXCLUDED_STATUSES constant in MrrCalculator
  instead of hardcoded array

DRY consolidation:
- calculate_churn delegates to calculate_churn_rate_for_period
- calculate_churned_customers delegates to calculate_churned_subscribers_in_period
- calculate_churned_mrr delegates to calculate_churned_mrr_in_period
- calculate_new_mrr delegates to calculate_new_mrr_in_period
- calculate_new_subscribers delegates to calculate_new_subscribers_in_period
- Add subscriptions_with_processor helper to avoid repeating
  .includes(:customer).select(...).joins(:customer)

Dashboard performance optimization:
- Batch monthly_summary: 72 queries → 5 (bulk load all subscription
  data for full 12-month range, group by month in Ruby)
- Batch daily_summary: 60 queries → 2 (same pattern for 30 days)
- Add period_data method: computes all period metrics in one pass,
  reusing intermediate values (9 → 6 queries per period)
- Move all computation from view to controller (precompute as instance
  variables, eliminating duplicate mrr_growth_rate calls)
- Verified on LicenseSeat: 38 queries, 2 cached, 71ms total

New features:
- monthly_summary(months:) and daily_summary(days:) public methods
- period_data(in_the_last:) public method returning hash of NumericResult
  values (new_customers, churned_customers, churn, new_mrr, churned_mrr,
  mrr_growth, revenue)

Test sync & coverage:
- Full sync of test_helper.rb Profitable module with production code
- 14 new tests (monthly_summary, daily_summary, period_data,
  new_subscribers filtering)
- 249 tests, 382 assertions, 0 failures, 94.64% coverage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link

claude bot commented Feb 9, 2026


Code review

No issues found. Checked for bugs and CLAUDE.md compliance.


@rameerez rameerez changed the title Fix bugs, DRY up code, and optimize dashboard (~176 → 38 queries) Fix bugs, DRY up code, add monthly/daily summaries, and optimize dashboard (~176 → 38 queries) Feb 10, 2026
@rameerez
Copy link
Owner Author

New Features: Monthly & Daily Summary Tables

This PR also adds two new dashboard sections and their corresponding public API methods:

Monthly Summary (last 12 months)

Profitable.monthly_summary(months: 12) returns an array of hashes, one per month (oldest first), each containing:

Key Type Description
:month String "2026-01" format
:month_date Time Beginning of month
:new_subscribers Integer Distinct customers with new non-trialing/paused subscriptions
:churned_subscribers Integer Distinct customers whose subscriptions ended (canceled/ended)
:net_subscribers Integer new - churned
:new_mrr Integer MRR from new active subscriptions (cents)
:churned_mrr Integer MRR lost from churned subscriptions (cents)
:net_mrr Integer new_mrr - churned_mrr
:churn_rate Float Churned / subscribers at start of month (%)

Daily Summary (last 30 days)

Profitable.daily_summary(days: 30) returns an array of hashes, one per day (oldest first):

Key Type Description
:date Date The date
:new_subscribers Integer New non-trialing/paused subscribers that day
:churned_subscribers Integer Subscribers who churned that day

Dashboard UI

Both summaries render as tables in the dashboard view with:

  • Color-coded positive (green) and negative (red) values
  • MRR amounts shown alongside subscriber counts
  • Newest entries at the top (.reverse in the view)

These are the "Monthly summary" and "Daily summary" sections visible between the top metric cards and the period breakdowns.

@rameerez rameerez merged commit a779db7 into main Feb 10, 2026
25 checks passed
@rameerez rameerez deleted the fix-bugs-dry-optimize-dashboard branch February 10, 2026 00:06
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