Table of Contents

Reporting

PolarSharp.Reporting exposes per-tenant analytics over Polar's order, subscription, customer, refund, and event data. Two flavours of API:

  • Aggregate / KPI reports — single roll-up for a dashboard tile (transactions, subscriptions, orders, customers, error audit, customer entitlements)
  • Hierarchical drilldown — three lazy methods designed for Telerik / MudBlazor / Blazor hierarchical grids

Plus JSON variants of every aggregate method (return pre-serialised JSON — no double round-trip through DTO mapping).

Aggregate / KPI reports

var result = await client.GetTransactionsAsync(new TransactionReportRequest
{
    PeriodStart = DateTimeOffset.UtcNow.AddMonths(-1),
    PeriodEnd = DateTimeOffset.UtcNow,
    Currency = "USD",
    Granularity = TimeBucketGranularity.Daily,
});

Returns TransactionReport with GrossRevenue / RefundedAmount / NetRevenue / OrderCount / AverageOrderValue / TopProducts / TimeBuckets.

Hierarchical drilldown

Three lazy methods that map to a hierarchical grid's expand-on-demand pattern:

// Top level — paged customers grid with pre-aggregated columns
var customers = await client.ListCustomersAsync(new CustomerListRequest { Page = 0, PageSize = 50 });

// Mid level — invoked when operator expands a customer row
var orders = await client.ListOrdersForCustomerAsync(customerId, new OrderListRequest { Page = 0 });

// Bottom level — invoked when operator opens a specific order
var detail = await client.GetOrderDrilldownAsync(orderId);
// detail.LineItems, detail.Refunds, detail.BenefitGrants all populated in one call

Why three methods, not one nested shape

An eager nested shape (CustomersWithOrdersWithLineItems) sized for a tenant with 10k customers OOMs the host and ships the entire dataset over JSON every page load. Telerik's hierarchical grids fetch detail rows on-demand precisely so they DON'T do this. Lazy fetching aligns with how dashboards actually behave: the operator views one or two customers at a time and opens at most a handful of orders. Memory + Polar API cost scales with what's viewed, not with the total table.

Pre-aggregated columns

CustomerListRow carries OrderCount, LifetimeValue, FirstOrderAt, LastOrderAt. OrderSummaryRow carries LineItemCount, RefundedAmount. These columns are maintained on the snapshot tables on every snapshot tick, so the top-level grid renders without per-row roll-up queries — even on tenants with 10k+ customers loading the first page in tens of milliseconds.

Snapshot vs Polar-API mode

When the optional IReportSnapshotService is enabled (PolarSharp:Reporting:EnableSnapshot=true), reports read from local indexed SQL — the snapshot service mirrors Polar's /v1/events/ / /v1/orders/ / /v1/subscriptions/ / /v1/customers/ into local tables on a schedule (default every 15 minutes). When the service is off, reports fall back to live Polar API calls with PolarSharp's pagination + circuit breaker. Production posture: enable snapshots.

Export

IReportExporter.ExportCsvAsync<T>(rows, stream) / ExportJsonAsync<T>(...) stream rows directly to a Stream — never buffers the whole result in memory. Use from the host's "download report" endpoint.

Snapshot schema

8 EF entities back the reporting layer: ReportEventEntity, ReportOrderEntity, ReportOrderLineItemEntity, ReportOrderRefundEntity, ReportSubscriptionEntity, ReportCustomerEntity, ReportBenefitGrantEntity, ReportSnapshotCheckpointEntity (the per-tenant per-resource ingestion cursor). Indexes are tuned for the drilldown paging pattern — (TenantId, LastOrderAt) on customers, (TenantId, CustomerId, CreatedAt) on orders, (TenantId, OrderId) on line items / refunds / grants.