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 aggregates credits across all three pools — op-specific tier allowances, general plan credits, and purchased credits — into a single dashboard with a per-operation breakdown.

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 billing period start
pub cycle_end: String, // RFC-3339 billing period end
pub credits_granted: i32, // Snapshot from org_budget.initial_included_credits
pub credits_spent: i32, // Total credits across all three pools
pub included_credits_remaining: i32, // Plan pool balance (clamped at 0)
pub credits_purchased_this_cycle: i32, // Credits bought this cycle (inflow)
pub purchased_credits_spent: i32, // Credits consumed from purchased pool (outflow)
pub by_operation: Vec<OpUsageRow>, // Sorted by highest spend first
pub overdraft_used: i32, // Current overdraft debt (0 when not in overdraft)
pub overdraft_limit: i32, // System-wide overdraft ceiling
}
pub struct OpUsageRow {
pub operation_type: OperationType,
pub credits_spent: i32, // Credits across all three pools for this op
}

The endpoint sums credits from all three pools per operation type:

PoolColumnSource
Op-specific tier allowancefree_units_usedPer-op credits included with the tier
General plan creditsincluded_creditsSeat-based plan allocation
Purchased creditspurchased_creditsCredit packs bought by the org
SELECT
operation_type,
SUM(included_credits) AS included_sum,
SUM(purchased_credits) AS purchased_sum,
SUM(free_units_used) AS free_units_sum
FROM operation
WHERE organization_id = ? AND created_at >= ?
GROUP BY operation_type

The credits_granted field reads from org_budget.initial_included_credits — a snapshot of the gross tier credit grant frozen at cycle start. This includes both seat-based credits and the sum of all per-op allowances from the tier.

let credits_granted = budget_credits
.map(|(_, _, initial)| initial.max(0))
.unwrap_or(0);

The snapshot is written by reset_cycle_budget_service::reset_cycle_budget_in_txn() on every cycle reset:

let gross_tier_allowance_i64 = (tier.credits_per_seat as i64)
.saturating_mul(seat_count as i64)
.saturating_add(sum_tier_per_op_allowances(&tier));
budget.initial_included_credits = Set(gross_tier_allowance);

This anchors both the numerator (included_credits_remaining) and denominator (credits_granted) to the same point in time. Admin tier edits mid-cycle no longer corrupt the “Credits included: X / Y” display — the denominator stays fixed until the next cycle reset.

Two distinct metrics track purchased credits:

  • credits_purchased_this_cycle — what was bought this cycle (from credit_pack_purchase table)
  • purchased_credits_spent — what was consumed from the purchased pool (from operation table)

These answer different questions: “How much did we buy?” vs. “How much of our purchased credits did we use?”

  1. Load subscription (no tier join needed — credits_granted comes from the budget snapshot)
  2. Load org_budget with select_only() (3 columns: included_credits, purchased_credits, initial_included_credits)
  3. Aggregate operation table with GROUP BY operation_type bounded by cycle start
  4. Aggregate credit_pack_purchase for credits bought this cycle
  5. Sort results by credits_spent descending

Returns OrgUsageSummary::default() when no subscription exists.

Three helper functions in src/mods/billing/services/record_operation_service.rs ensure consistent arithmetic across the customer and admin billing APIs.

Safely narrows i64 aggregates to i32. Clamps to i32::MAX and emits a structured warn log on overflow.

pub(crate) fn clamp_i64_to_i32(
value: i64,
label: &'static str,
org_id: Uuid,
op_type: Option<OperationType>,
) -> i32

Use this whenever DB SUM() aggregates or multiplicative totals need to fit in an i32 API response field.

Sums all per-operation credit allowances from a subscription_tier row. Returns the total as i64.

pub(crate) fn sum_tier_per_op_allowances(
tier: &schemas::subscription_tier::Model,
) -> i64

Enumerates every *_included field on the tier (voice, SMS, AI tiers, email, etc.). Update this in lockstep with tier_allowance() when adding new OperationType variants.

Returns the positive magnitude of a negative included_credits balance — the amount currently owed against overdraft.

pub(crate) fn overdraft_depth(included_credits: i32) -> i32 {
(-included_credits).max(0)
}

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
├─ OverdraftBanner ← conditional: overdraft_used > 0
├─ Three-column card
│ ├─ Credits used ← total spent, with plan/purchased split
│ ├─ Credits included ← remaining / granted + progress bar
│ └─ Credits purchased ← bought this cycle
├─ Empty state card ← when by_operation is empty
└─ OperationsBreakdown ← table of OpUsageRow items
└─ OperationBreakdownRow

The card displays three balanced stats:

ColumnValueDetail
Credits usedcredits_spentInline split: (X from plan · Y purchased) when purchased > 0
Credits includedincluded_credits_remaining / credits_grantedProgress bar tracking plan pool depletion
Credits purchasedcredits_purchased_this_cycleCredits bought this cycle (inflow, not outflow)

On mobile, columns stack vertically.

The bar tracks plan pool depletion only — purchased credit spend does not advance it.

let progress = if credits_granted > 0 {
(1.0 - (included_credits_remaining as f64 / credits_granted as f64))
.clamp(0.0, 1.0)
} else {
0.0
};
  • 0% — fresh cycle, full plan pool
  • 100% — plan pool fully consumed
  • Overdraft is communicated by a separate OverdraftBanner, not the progress bar

The breakdown groups operations into two categories:

Non-AI rows render flat — one row per OperationType showing credits and share %.

AI rows (all 24 Ai* variants plus AiTextLegacy) collapse under a single “AI usage” row. This row shows the aggregated AI credit total and its share of the overall cycle spend. Click the chevron (>) to expand per-feature rows sorted by spend descending, with share percentages computed against the AI total (not the overall total). Click again to collapse.

Each row shows:

  • Operation name — derived from OperationType display
  • Credits — total credits across all three pools
  • Share % — relative to overall total (top-level) or AI total (expanded detail rows)

The collapsible layout is mobile-responsive — indented detail rows stack cleanly on narrow viewports.

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.

The admin billing API (get_admin_org_billing_api) uses the same three-pool aggregation and shared helpers. Both endpoints produce consistent credit totals.