php 135 lines · 4 tabs

Aggregate Daily Sales into a Summary Table with a Laravel Console Command

Shared by codesnips Aug 2026
4 tabs
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('daily_sales_summaries', function (Blueprint $table) {
            $table->id();
            $table->date('summary_date')->unique();
            $table->unsignedInteger('orders_count')->default(0);
            $table->decimal('gross_total', 12, 2)->default(0);
            $table->decimal('refunds_total', 12, 2)->default(0);
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('daily_sales_summaries');
    }
};
4 files · php Explain with highlit

This snippet shows how a recurring reporting rollup is built in Laravel: raw order rows are collapsed into one summary row per day so dashboards read from a small, indexed table instead of scanning the full orders table on every request. The pattern is a classic pre-aggregation, trading a little write-time work and some data staleness for cheap, predictable reads.

The create_daily_sales_summaries_table migration defines the target table. Each row is keyed by summary_date with a unique constraint, which is the linchpin of the whole design: it lets the command re-run for the same day without creating duplicates, using an idempotent upsert. Money is stored as decimal to avoid float rounding, and orders_count plus refunds_total give enough detail for a basic revenue view.

The DailySalesSummary model is a thin Eloquent model. summary_date is cast to a date and the netRevenue() accessor derives net figures on read rather than storing a redundant column that could drift out of sync. The forRange() scope keeps reporting queries expressive and reusable.

The heart of the feature is AggregateDailySales command. Its handle() method resolves a target day from the optional --date argument, defaulting to yesterday so a nightly run summarizes a complete day. It builds a single grouped aggregate query with selectRaw over the orders table, computing SUM and COUNT in the database — far cheaper than pulling rows into PHP. The result is written with updateOrCreate, matched on summary_date, so running the command twice simply overwrites the day's totals; this makes backfills and retries safe.

Because the query only touches paid orders within a half-open [start, end) interval, it avoids double-counting boundary timestamps and naturally ignores pending or cancelled orders. The --date argument also enables backfilling historical days in a loop from a shell.

The Console schedule registration wires the command into Laravel's scheduler with dailyAt('01:15') and withoutOverlapping(), so a long-running rollup never stacks on top of itself. Reaching for this approach makes sense once report queries grow slow; the main trade-offs are eventual consistency and the need to re-aggregate when historical orders change.


Related snips

Share this code

Here's the card — post it anywhere.

Aggregate Daily Sales into a Summary Table with a Laravel Console Command — share card
Link copied