Developer Docs

Analytics Aggregation API โ€” Developer Guide

Overview

The Analytics Aggregation API provides platform administrators with real-time insights into revenue performance, order volume trends, and entity counts across the entire Mallnline platform. Exposed through the murchases subgraph (apps/murchases), these admin-only queries use MongoDB aggregation pipelines to compute metrics server-side with full multi-currency support.

Component Purpose Backend
AnalyticsResolver 4 admin-only aggregation queries @Resolver() with dual-layer access
AnalyticsService MongoDB $group, $dateToString, $sort pipelines Order model aggregation
AnalyticsFilter Date range presets + custom filters AnalyticsDateRange enum

All operations are gated behind dual-layer access control: the @RequirePermission(Permission.MANAGE_ANALYTICS) decorator guard and a runtime PLATFORM_ADMIN role check.


Architecture

graph TD
    A["AnalyticsResolver"] --> B["AnalyticsService"]
    B --> C["Order Model (MongoDB)"]

    A -- "admin header" --> D["@RequirePermission(MANAGE_ANALYTICS)"]
    A -- "runtime" --> E["assertPlatformAdmin(actor)"]

    subgraph "Aggregation Pipelines"
        C -- "$group by date + currency" --> F["revenueByDay"]
        C -- "$group by date + status" --> G["orderVolumeTrends"]
        C -- "$group by status + countDocuments" --> H["platformEntityCounts"]
        C -- "$group by maletId + currency, $sort" --> I["topMaletsByRevenue"]
    end

    style D fill:#dc2626,color:#fff
    style E fill:#dc2626,color:#fff
    style F fill:#3b82f6,color:#fff
    style G fill:#3b82f6,color:#fff
    style H fill:#3b82f6,color:#fff
    style I fill:#3b82f6,color:#fff

How It Fits Together

The murchases subgraph owns the Order entity โ€” the authoritative source for all transaction data, including amounts, currencies, statuses, and Malet associations. The analytics API adds a privileged aggregation layer over this data:

  • Multi-currency: Revenue is grouped by currency natively. A Kenyan Malet selling in KES will never have its revenue mixed with a South African Malet selling in ZAR. When multiple currencies are present without a filter, the summary reports "MIXED".
  • Date range presets: Five preset ranges (LAST_7_DAYS, LAST_30_DAYS, LAST_90_DAYS, LAST_365_DAYS, CUSTOM) with custom startDate/endDate overrides. Default is last 30 days.
  • Cross-service stubs: Entity counts for Products, Services, Malets, and Users default to 0 and are designed for future federation enrichment by owning subgraphs.

GraphQL API

Revenue By Day

The primary revenue query returns daily breakdowns grouped by currency:

query RevenueByDay($filter: AnalyticsFilter) {
	revenueByDay(filter: $filter) {
		days {
			date
			totalRevenue
			orderCount
			currency
		}
		totalRevenue
		totalOrders
		averageOrderValue
		currency
	}
}

Variables:

{
	"filter": {
		"dateRange": "LAST_30_DAYS",
		"maletId": "malet-ke-safari",
		"currency": "KES"
	}
}

The currency field in RevenueSummary reports "MIXED" when multiple currencies are returned without a currency filter. This prevents accidental mixing of KES and USD amounts in summary totals.

Daily order volume grouped by day and status โ€” ideal for stacked bar charts:

query OrderVolumeTrends($filter: AnalyticsFilter) {
	orderVolumeTrends(filter: $filter) {
		trends {
			date
			count
			status
		}
		periodStart
		periodEnd
		totalOrders
	}
}

Each trend entry includes the OrderStatus enum value (PENDING, PROCESSING, SHIPPED, DELIVERED, CANCELLED), enabling visualization of order lifecycle distribution over time.

Platform Entity Counts

A lightweight dashboard snapshot of platform-wide metrics:

query {
	platformEntityCounts {
		totalOrders
		totalRevenue
		ordersByStatus {
			status
			count
		}
		totalProducts
		totalServices
		totalMalets
		totalUsers
	}
}

Note: totalProducts, totalServices, totalMalets, and totalUsers are currently 0 stubs. When owning subgraphs (products, services, malets, nodes) add @provides fields, the Gateway automatically composes real values via GraphQL Federation.

Top Malets by Revenue

Revenue leaderboard ranked by Malet, grouped by currency:

query TopMalets($limit: Int, $filter: AnalyticsFilter) {
	topMaletsByRevenue(limit: $limit, filter: $filter) {
		maletId
		totalRevenue
		orderCount
		currency
	}
}

Variables:

{
	"limit": 10,
	"filter": {
		"dateRange": "LAST_90_DAYS",
		"currency": "ZAR"
	}
}

Results are sorted by totalRevenue descending. The limit parameter caps at 100.


Type Reference

AnalyticsFilter (Input)

Field Type Default Description
dateRange AnalyticsDateRange LAST_30_DAYS Preset date range. Ignored when custom dates are set.
startDate DateTime โ€” Custom start date (overrides preset)
endDate DateTime โ€” Custom end date (overrides preset)
maletId ID โ€” Filter to a specific Malet
currency String โ€” Filter by currency code (e.g., KES, ZAR, USD)

AnalyticsDateRange (Enum)

enum AnalyticsDateRange {
  LAST_7_DAYS    // 7-day window
  LAST_30_DAYS   // 30-day window (default)
  LAST_90_DAYS   // Quarterly view
  LAST_365_DAYS  // Annual view
  CUSTOM         // Use startDate/endDate
}

RevenueSummary

Field Type Description
days [RevenueByDay!]! Daily revenue breakdown
totalRevenue Int! Total revenue in minor units (cents/pence)
totalOrders Int! Total Murchases in the period
averageOrderValue Float! Mean order value in minor units
currency String! Currency code or "MIXED" if multiple

RevenueByDay

Field Type Description
date DateTime! Calendar date
totalRevenue Int! Revenue in minor units for this day
orderCount Int! Number of Murchases placed
currency String! Currency code (e.g., KES)

OrderVolumeSummary

Field Type Description
trends [OrderVolumeTrend!]! Daily trend data
periodStart DateTime! Start of the queried period
periodEnd DateTime! End of the queried period
totalOrders Int! Total Murchases in the period

PlatformEntityCounts

Field Type Source Description
totalOrders Int! Local (Order model) All-time Murchase count
totalRevenue Int! Local (Order model) All-time revenue in minor units
ordersByStatus [StatusCount!]! Local (Order model) Count per status
totalProducts Int! Federation stub 0 until products subgraph contributes
totalServices Int! Federation stub 0 until services subgraph contributes
totalMalets Int! Federation stub 0 until malets subgraph contributes
totalUsers Int! Federation stub 0 until nodes subgraph contributes

TopMaletRevenue

Field Type Description
maletId ID! Malet identifier
totalRevenue Int! Revenue in minor units
orderCount Int! Number of Murchases
currency String! Currency code

Multi-Currency Design

Mallnline supports Malets across different countries, each selling in their local currency:

Region Currency Code
Kenya Kenyan Shilling KES
South Africa South African Rand ZAR
United States US Dollar USD
Nigeria Nigerian Naira NGN

Revenue aggregation never mixes currencies. The $group stage groups by both date and currency, ensuring a Malet selling in KES is reported separately from one selling in USD.

When querying without a currency filter, the RevenueSummary.currency field returns "MIXED" to alert the consumer that the totalRevenue sum spans multiple denominations. Use the currency filter to get accurate single-currency totals:

# Get USD-only revenue
query {
	revenueByDay(filter: { currency: "USD" }) {
		totalRevenue
		currency # "USD"
	}
}

# Get all currencies (MIXED warning)
query {
	revenueByDay {
		totalRevenue
		currency # "MIXED"
		days {
			currency # Each day shows its own currency
		}
	}
}

Access Control

Dual-Layer Security

Identical to the User Listing API pattern:

  1. Declarative guard โ€” @RequirePermission(Permission.MANAGE_ANALYTICS)
  2. Runtime check โ€” assertPlatformAdmin(actor) verifies role === 'PLATFORM_ADMIN'
@Query(() => RevenueSummary)
@RequirePermission(Permission.MANAGE_ANALYTICS)
@HandleErrors()
async revenueByDay(
  @Args('filter', { nullable: true }) filter: AnalyticsFilter,
  @CurrentActor() actor: Actor,
): Promise<RevenueSummary> {
  this.assertPlatformAdmin(actor);  // Defense-in-depth
  return this.analyticsService.getRevenueByDay(filter);
}

Permission Separation

Permission Scope Use Case
VIEW_ANALYTICS Malet Owner View own Malet's analytics dashboard
MANAGE_ANALYTICS Platform Admin View global platform-wide analytics

Module Structure

apps/murchases/src/analytics/
โ”œโ”€โ”€ analytics.types.ts          # GraphQL types, enums, filter input
โ”œโ”€โ”€ analytics.service.ts        # MongoDB aggregation pipelines
โ”œโ”€โ”€ analytics.service.spec.ts   # 20 unit tests
โ”œโ”€โ”€ analytics.resolver.ts       # Admin-only resolver (4 queries)
โ”œโ”€โ”€ analytics.resolver.spec.ts  # 10 unit tests
โ””โ”€โ”€ analytics.module.ts         # NestJS module (Order model import)

apps/murchases/test/
โ””โ”€โ”€ analytics.e2e-spec.ts       # 9 E2E tests (multi-currency)

Testing

Unit Tests

# Run analytics unit tests (30 tests across 2 suites)
npm run test -- apps/murchases --testPathPattern="analytics"

Key coverage areas:

  • AnalyticsService: Date range resolution (all presets + custom), revenue aggregation by day, multi-currency grouping (MIXED label), filter pass-through (maletId, currency, date range), order volume trends by status, entity counts with status breakdown, top Malets ranking, limit capping (max 100), empty results handling
  • AnalyticsResolver: Admin access control (pass/fail/unauthenticated), query delegation, filter forwarding

E2E Tests

# Run analytics E2E tests (9 tests)
npx jest --config apps/murchases/test/jest-e2e.json --testPathPattern="analytics" --detectOpenHandles

Covers: Revenue by day with seeded multi-currency orders (USD + KES), MIXED currency detection, order volume trends, platform entity counts with cross-service stubs, top Malets ranking, and admin access enforcement on all 4 queries.


Cross-Service Integration

Dependency Map

Analytics depends on For
Order model (Typegoose) Transaction data, amounts, currencies, statuses
@app/common Permission.MANAGE_ANALYTICS, RequirePermission, CurrentActor, HandleErrors

Federation Enrichment (Future)

When owning subgraphs contribute @provides annotations to the PlatformEntityCounts type:

Subgraph Field Mechanism
products totalProducts countDocuments on Item model
services totalServices countDocuments on Service model
malets totalMalets countDocuments on Malet model
nodes totalUsers countDocuments on User model