Files
textbee/web/lib/usage.test.ts
isra el a949729c07 feat: rebuild dashboard home around real usage and activity data
The home page was four all-time counters that could not answer either
question a user actually opens the dashboard for: how much quota is left, and
did my recent sends work.

Removed fabricated data:
- Every stat card rendered a green TrendingUp arrow whenever a value existed.
  Nothing computed a trend. The stats endpoint returns running totals with no
  time window, so there was nothing to compare against.
- "Since last year" on the sent and received counts. getStatsForUser sums
  device counters with no date filter, so these are all-time totals.
- "Active Devices" with a "Connected now" caption, over a number that counts
  every device including disabled ones. Enabled count is now derived from the
  device list we already fetch.
- "Active keys" over a count that included revoked keys; now uses the active
  API key list.

Added, all from fields the API already returns:
- Usage cards leading the page: today and this month against their limits,
  with progress bars, an amber near-limit state past 80% and an upgrade link.
  Unlimited plans (-1) show usage with no meter, since there is no ceiling to
  measure against.
- Recent activity: the last 5 messages for the connected device. Messages are
  only exposed per device, so the card names the device rather than implying
  account-wide coverage.
- All-time totals compacted from four large cards into one honest strip.

lib/usage.ts extracts the daily/monthly derivation that subscription-info
already did inline, so the dashboard and billing page cannot disagree about
remaining quota. Unit tested including the -1 sentinel, custom limit
overrides, and an over-100 percentage (possible when a limit is lowered
mid-period).

No backend changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 20:32:59 +03:00

86 lines
2.4 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { deriveUsage, UNLIMITED } from './usage'
describe('deriveUsage', () => {
it('reads the daily and monthly windows from usage', () => {
const { daily, monthly } = deriveUsage({
plan: { dailyLimit: 500, monthlyLimit: 5000 },
usage: {
dailyLimit: 500,
monthlyLimit: 5000,
processedSmsToday: 47,
processedSmsLastMonth: 1204,
dailyRemaining: 453,
monthlyRemaining: 3796,
dailyUsagePercentage: 9,
monthlyUsagePercentage: 24,
},
})
expect(daily.used).toBe(47)
expect(daily.limit).toBe(500)
expect(daily.remaining).toBe(453)
expect(daily.percentage).toBe(9)
expect(daily.unlimited).toBe(false)
expect(monthly.used).toBe(1204)
expect(monthly.percentage).toBe(24)
})
it('treats -1 as unlimited and suppresses the percentage', () => {
const { daily } = deriveUsage({
plan: { dailyLimit: UNLIMITED },
usage: { dailyLimit: UNLIMITED, processedSmsToday: 900 },
})
expect(daily.unlimited).toBe(true)
expect(daily.percentage).toBe(0)
expect(daily.nearLimit).toBe(false)
expect(daily.atLimit).toBe(false)
// Usage is still reported, there is just nothing to measure it against.
expect(daily.used).toBe(900)
})
it('prefers usage limits over plan limits (custom overrides)', () => {
const { daily } = deriveUsage({
plan: { dailyLimit: 50 },
usage: { dailyLimit: 5000, processedSmsToday: 100 },
})
expect(daily.limit).toBe(5000)
})
it('flags the near-limit threshold', () => {
const { daily } = deriveUsage({
usage: { dailyLimit: 100, processedSmsToday: 85, dailyUsagePercentage: 85 },
})
expect(daily.nearLimit).toBe(true)
expect(daily.atLimit).toBe(false)
})
it('flags being at the limit and clamps an over-100 percentage', () => {
// Happens when a limit is lowered mid-period.
const { daily } = deriveUsage({
usage: {
dailyLimit: 100,
processedSmsToday: 150,
dailyUsagePercentage: 150,
},
})
expect(daily.percentage).toBe(100)
expect(daily.atLimit).toBe(true)
expect(daily.nearLimit).toBe(false)
})
it('is safe on an undefined subscription', () => {
const { daily, monthly } = deriveUsage(undefined)
expect(daily.used).toBe(0)
expect(daily.limit).toBeUndefined()
expect(daily.unlimited).toBe(false)
expect(monthly.used).toBe(0)
})
})