Skip to content

Cycle Usage View

The cycle usage view gives org members a real-time look at credit consumption on the billing settings tab. It shows credits spent vs. included, a per-operation breakdown, and free-allowance progress — all without admin access.

Returns aggregated usage for the current billing cycle. No auth gate — any org member can call it (matches the get_org_subscription_api access pattern).

Handler: get_org_usage_api() in src/mods/billing/api/get_org_usage_api.rs

Response type: OrgUsageSummary

pub struct OrgUsageSummary {
pub has_subscription: bool,
pub cycle_start: String, // RFC-3339 timestamp
pub cycle_end: String, // RFC-3339 timestamp
pub credits_granted: i32, // tier.credits_per_seat × seat_count
pub credits_spent: i32, // Total credits deducted this cycle
pub credits_remaining: i32, // included_credits + purchased_credits
pub by_operation: Vec<OpUsageRow>, // Sorted by highest spend first
}
pub struct OpUsageRow {
pub operation_type: OperationType,
pub units: Decimal, // Raw billed units (native scale)
pub free_units_used: Decimal, // Units absorbed by free allowance
pub credits_spent: i32, // Credits charged for this op type
pub tier_free_allowance: Option<i32>, // None = pay-per-use; Some(0) = not allowed
}

Query strategy:

  1. Load subscription + tier via find_also_related() (single join)
  2. Load org_budget with select_only() (2 columns: included_credits, purchased_credits)
  3. Aggregate operation table with GROUP BY operation_type bounded by cycle dates
  4. Sort results by credits_spent descending

Returns OrgUsageSummary::default() (with has_subscription: false) when no subscription exists.

Two methods on OperationType convert stored units to customer-friendly display values:

impl OperationType {
/// Customer-facing unit label (always plural)
pub fn display_unit_label(&self) -> &'static str {
// Voice/Recording/Transcription/Realtime → "minutes"
// SMS → "segments", MMS → "messages"
// AI text (all tiers) → "tokens"
// Email → "emails"
}
/// Multiplier from stored units to display units
pub fn display_unit_multiplier(&self) -> i64 {
// AI text: 1000 (stored as 1K-token blocks)
// Everything else: 1
}
}

Example: An AI text operation stored as units = 12 (1K-token blocks) displays as 12,000 tokens.

CycleUsageSection renders inside BillingContent, below the current-plan card. It only appears when a subscription exists.

File: src/mods/billing/components/cycle_usage_section_component.rs

CycleUsageSection ← exported, no props, owns use_resource
└─ CycleUsageContent ← receives OrgUsageSummary
├─ ProgressBar ← credits_spent / credits_granted
├─ Empty state card ← when by_operation is empty
└─ OperationsBreakdown ← table of OpUsageRow items
└─ OperationBreakdownRow ← one per operation type
let usage = use_resource(|| async move { get_org_usage_api().await });
match &*usage.read() {
None => /* ViewLoader spinner */,
Some(Err(e)) => /* ResourceError */,
Some(Ok(s)) if !s.has_subscription => /* nothing */,
Some(Ok(s)) => /* CycleUsageContent */,
}

The denominator handles edge cases:

let denominator = if credits_granted > 0 {
credits_granted
} else {
credits_spent + credits_remaining // Fallback for legacy tiers
};
let progress = (credits_spent as f64 / denominator as f64).clamp(0.0, 1.0);

Each OperationBreakdownRow shows:

  • Operation name — derived from OperationType display
  • Native units — scaled via display_unit_multiplier(), formatted with format_with_commas()
  • Credits — raw credit count
  • Share %(row.credits_spent / summary.credits_spent * 100).round()
  • Free allowance hint"12 of 100 free minutes used" when tier_free_allowance > 0
  • Desktop (≥640px): 4-column grid with header row (Operation, Usage, Credits, Share)
  • Mobile (under 640px): Stacked layout, inline labels per row

When the org has a subscription but zero operations this cycle, a dashed-border card displays: “No usage this cycle yet” with a hint that activity appears once calls, messages, or AI actions run.

format_with_commas() in src/shared/utils/format_with_commas.rs adds thousands separators to numeric strings. Moved here from the billing module for reuse across the app.