ruby 86 lines · 3 tabs

Aggregate Monthly Sales Reports in Rails with group_by SQL and a Presenter

Shared by codesnips Jul 2026
3 tabs
class ReportsController < ApplicationController
  def monthly_sales
    year = params.fetch(:year, Date.current.year).to_i
    rows = Order.monthly_sales(year: year)
    @report = MonthlySalesReport.new(rows, year: year)

    respond_to do |format|
      format.html
      format.json do
        render json: {
          year: year,
          total_revenue: @report.total_revenue,
          lines: @report.lines
        }
      end
    end
  end
end
3 files · ruby Explain with highlit

This snippet shows how a monthly sales report is built end to end in Rails: a single grouped aggregate query, a plain Ruby presenter that shapes the raw numbers for a view, and the controller that wires them together. The goal is to push the heavy lifting into the database and keep Ruby responsible only for presentation, which is the split that keeps reporting endpoints fast and readable.

In Order model, the class method monthly_sales is the core of the report. It uses date_trunc('month', ...) in a group clause so Postgres collapses every order into one row per calendar month, and it computes SUM, COUNT, and AVG in the same pass. The query returns a hash keyed by the truncated timestamp because group(...).sum(...) alone can only carry one aggregate; instead the code selects an explicit column list and calls unscope(:order) so no default scope leaks a stray ORDER BY into the grouped query. Filtering happens with a completed scope and a bounded date range, so the index on (status, created_at) can be used and partial months at the edges are excluded predictably.

Each result row is a lightweight Struct (MonthlyRow) rather than a full record, which avoids instantiating thousands of Order objects just to read three numbers — a common performance pitfall when aggregation is done in Ruby with group_by on a loaded collection.

In MonthlySalesReport presenter, the raw rows are decorated for display. It fills gaps so months with zero sales still appear, formats currency and percentages, and derives growth_pct by comparing each month to the previous one. Keeping this logic out of the model means the same query can feed a CSV export or a chart without duplicating formatting rules, and the presenter stays trivial to unit test because it takes plain data in and returns plain data out.

In ReportsController, the action parses the requested year, delegates the query to Order.monthly_sales, wraps the result in the presenter, and responds with either HTML or JSON. The controller holds no business logic — it only coordinates. This layering (query in the model, formatting in the presenter, coordination in the controller) is the pattern to reach for whenever a report grows beyond a one-off sum, because it isolates the parts that change for different reasons.


Related snips

Share this code

Here's the card — post it anywhere.

Aggregate Monthly Sales Reports in Rails with group_by SQL and a Presenter — share card
Link copied