Order Analytics & Reporting
The Orders module provides real-time analytics via the OrderAnalyticsService with PostgreSQL materialized views for high-performance reporting at scale.
API Endpoints
All analytics endpoints accept from and to query parameters for date range filtering:
?from=2026-01-01&to=2026-03-31
Dashboard Metrics
GET /api/v1/orders/analytics/dashboard?from=2026-03-01&to=2026-03-31
Response:
{
"data": {
"total_orders": 1250,
"total_revenue": 3750000.00,
"avg_order_value": 3000.00,
"unique_customers": 890,
"by_status": {
"pending": 45,
"confirmed": 120,
"processing": 80,
"shipped": 200,
"delivered": 750,
"cancelled": 55
},
"by_payment_status": {
"paid": 800,
"cod": 350,
"pending": 45,
"partial": 10,
"refunded": 45
}
}
}
Revenue Time Series
Daily revenue and order counts:
GET /api/v1/orders/analytics/revenue?from=2026-03-01&to=2026-03-31
Response:
{
"data": [
{ "day": "2026-03-01", "orders": 42, "revenue": 126000.00 },
{ "day": "2026-03-02", "orders": 38, "revenue": 114000.00 },
{ "day": "2026-03-03", "orders": 55, "revenue": 165000.00 }
]
}
Fulfillment Metrics
Delivery, cancellation, and RTO rates:
GET /api/v1/orders/analytics/fulfillment?from=2026-03-01&to=2026-03-31
Response:
{
"data": {
"total": 1250,
"delivered": 750,
"cancelled": 55,
"rto": 30,
"delivery_rate": 60.0,
"cancellation_rate": 4.4,
"rto_rate": 2.4
}
}
Source Breakdown
Which modules/channels orders come from:
GET /api/v1/orders/analytics/sources?from=2026-03-01&to=2026-03-31
Response:
{
"data": [
{ "source_module": "store-shopify", "count": 800, "revenue": 2400000.00 },
{ "source_module": "manual", "count": 300, "revenue": 900000.00 },
{ "source_module": "csv-import", "count": 150, "revenue": 450000.00 }
]
}
Top Products
Products ranked by order volume:
GET /api/v1/orders/analytics/products?from=2026-03-01&to=2026-03-31&limit=10
Response:
{
"data": [
{ "name": "Premium Widget", "sku": "PW-001", "total_quantity": 320, "total_revenue": 960000.00 },
{ "name": "Basic Gadget", "sku": "BG-001", "total_quantity": 250, "total_revenue": 500000.00 }
]
}
COD vs Prepaid Split
GET /api/v1/orders/analytics/cod-vs-prepaid?from=2026-03-01&to=2026-03-31
Response:
{
"data": {
"cod_count": 350,
"prepaid_count": 800,
"cod_revenue": 1050000.00,
"prepaid_revenue": 2400000.00
}
}
Service Usage
use Modules\Orders\App\Services\OrderAnalyticsService;
$analytics = app(OrderAnalyticsService::class);
$from = '2026-03-01';
$to = '2026-03-31';
// Dashboard summary
$metrics = $analytics->getDashboardMetrics($from, $to);
// Revenue time series (daily)
$series = $analytics->getRevenueTimeSeries($from, $to);
// Source breakdown
$sources = $analytics->getSourceBreakdown($from, $to);
// Fulfillment rates
$fulfillment = $analytics->getFulfillmentMetrics($from, $to);
// COD vs Prepaid
$split = $analytics->getCodVsPrepaid($from, $to);
// Top 10 products
$products = $analytics->getTopProducts($from, $to, 10);
PostgreSQL Materialized Views
For high-traffic tenants, the module uses materialized views for pre-computed analytics. These are refreshed automatically every 15 minutes via RefreshAnalyticsViewsJob.
mv_daily_order_stats
Pre-aggregated daily statistics:
| Column | Type | Description |
|---|---|---|
day |
date | Date |
total_orders |
int | Orders placed |
total_revenue |
decimal | Total revenue |
avg_order_value |
decimal | Average order value |
cod_count |
int | COD orders |
prepaid_count |
int | Prepaid orders |
unique_customers |
int | Distinct customers |
mv_order_source_stats
Per-source daily statistics:
| Column | Type | Description |
|---|---|---|
source_module |
string | Order source |
day |
date | Date |
total_orders |
int | Orders from this source |
total_revenue |
decimal | Revenue from this source |
avg_order_value |
decimal | Average value from this source |
Refresh Strategy
// In OrdersServiceProvider
$this->app->afterResolving(Schedule::class, function (Schedule $schedule) {
$schedule->job(new RefreshAnalyticsViewsJob())
->everyFifteenMinutes()
->withoutOverlapping();
});
The views use REFRESH MATERIALIZED VIEW CONCURRENTLY which doesn't lock reads during refresh. This requires unique indexes on the views, which are created by the migration.
When to Use Views vs Live Queries
| Scenario | Approach |
|---|---|
| Dashboard widgets | Use materialized views for instant load |
| Date-range reports | Live queries (small result sets) |
| Real-time monitoring | Live queries |
| Historical exports | Materialized views + live queries |
The OrderAnalyticsService currently uses live queries by default. For tenants with 100K+ orders, switch the service to read from materialized views for dashboard metrics.
Database Indexes for Analytics
The following indexes optimize analytics queries:
| Index | Columns | Purpose |
|---|---|---|
| Composite | (status, created_at) |
Status-filtered date queries |
| Composite | (payment_status, created_at) |
Payment status reports |
| Composite | (source_module, created_at) |
Source breakdown queries |
| Composite | (customer_id, created_at) |
Per-customer analytics |
| BRIN | created_at |
Time-range scans (compact, efficient for ordered data) |
| Partial | WHERE status = 'pending' |
Fast count of pending orders |
| Partial | WHERE status = 'ready_to_ship' |
Fast count of ready-to-ship |