stack/apps/backend/src/lib/plan-usage.test.ts
Armaan Jain 80ba110e78
Usage page in settings (#1595)
## Summary

Adds a Usage page under Project Settings showing the owner team's plan,
billing period, and resource consumption (dashboard admins, auth users,
emails, analytics events, session replays) with progress bars and an
upgrade CTA.

Backend aggregates usage across the team via `sumTenancyUsage`
(parallelized with `Promise.all`) and serves it through `GET
/internal/plan-usage`. Shared types in `@hexclave/shared` define the
contract consumed by the SDK and dashboard.

## Screenshots

![Usage page — light
mode](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzE2ZTAyM2NkLTQzZjgtNDkyZS1hNDFkLTVmZjc1ZDg5NTQ3MSIsImlhdCI6MTc4MTU1MzY4OSwiZXhwIjoxNzgyMTU4NDg5LCJmaWxlbmFtZSI6InNjcmVlbnNob3RfZWE1YWU3YTJkNWQwNGM2NmFmYmM4NTY0YjQ2OTMxMDMucG5nIn0.O5H-gvyZ5an3wM7-CRcuyb6uFgg86cSftnAKnWh57VA)

Link to Devin session:
https://app.devin.ai/sessions/1bc3344126a442adb4f29ae373d346be
Requested by: @Developing-Gamer

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added Usage page in project settings displaying comprehensive plan
usage metrics including dashboard seats, authentication users, emails
sent, analytics events, and session replays
* Shows current usage against plan limits with visual progress
indicators
* Displays upgrade recommendations when plan limits are exceeded with
one-click upgrade functionality
  * Added Usage menu item to project settings navigation

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: armaan <armaan@stack-auth.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-23 10:32:07 -07:00

166 lines
4.3 KiB
TypeScript

import { ITEM_IDS, UNLIMITED } from "@hexclave/shared/dist/plans";
import type { SubscriptionRow } from "./payments/schema/types";
import { buildUsageRow, getNextPlanId, getPlanUsagePeriod } from "./plan-usage";
import { describe, expect, it } from "vitest";
function createSubscriptionPeriod(startMillis: number, endMillis: number): SubscriptionRow {
return {
id: "sub_1",
tenancyId: "tenancy_1",
customerId: "team_1",
customerType: "team",
productId: "team",
priceId: "monthly",
product: {
displayName: "Team",
customerType: "team",
prices: {},
includedItems: {},
},
quantity: 1,
stripeSubscriptionId: null,
status: "active",
currentPeriodStartMillis: startMillis,
currentPeriodEndMillis: endMillis,
cancelAtPeriodEnd: false,
canceledAtMillis: null,
endedAtMillis: null,
refundedAtMillis: null,
productRevokedAtMillis: null,
creationSource: "TEST_MODE",
createdAtMillis: startMillis,
};
}
describe("buildUsageRow", () => {
it("calculates remaining usage under the limit", () => {
expect(buildUsageRow({
itemId: ITEM_IDS.emailsPerMonth,
displayName: "Emails per month",
kind: "metered",
used: 25,
limit: 100,
})).toMatchInlineSnapshot(`
{
"display_name": "Emails per month",
"is_unlimited": false,
"item_id": "emails_per_month",
"kind": "metered",
"limit": 100,
"overage": 0,
"remaining": 75,
"used": 25,
}
`);
});
it("treats exact limit as no overage", () => {
expect(buildUsageRow({
itemId: ITEM_IDS.analyticsEvents,
displayName: "Analytics events",
kind: "metered",
used: 100,
limit: 100,
})).toMatchInlineSnapshot(`
{
"display_name": "Analytics events",
"is_unlimited": false,
"item_id": "analytics_events",
"kind": "metered",
"limit": 100,
"overage": 0,
"remaining": 0,
"used": 100,
}
`);
});
it("calculates overage when usage exceeds the limit", () => {
expect(buildUsageRow({
itemId: ITEM_IDS.sessionReplays,
displayName: "Session replays",
kind: "metered",
used: 125,
limit: 100,
})).toMatchInlineSnapshot(`
{
"display_name": "Session replays",
"is_unlimited": false,
"item_id": "session_replays",
"kind": "metered",
"limit": 100,
"overage": 25,
"remaining": 0,
"used": 125,
}
`);
});
it("represents unlimited auth users without remaining or overage", () => {
expect(buildUsageRow({
itemId: ITEM_IDS.authUsers,
displayName: "Auth users",
kind: "current",
used: 250_000,
limit: UNLIMITED,
})).toMatchInlineSnapshot(`
{
"display_name": "Auth users",
"is_unlimited": true,
"item_id": "auth_users",
"kind": "current",
"limit": null,
"overage": 0,
"remaining": null,
"used": 250000,
}
`);
});
});
describe("plan upgrade targets", () => {
it("selects the next paid tier", () => {
expect({
free: getNextPlanId("free"),
team: getNextPlanId("team"),
growth: getNextPlanId("growth"),
}).toMatchInlineSnapshot(`
{
"free": "team",
"growth": null,
"team": "growth",
}
`);
});
});
describe("billing period selection", () => {
it("uses the subscription period when available", () => {
const start = Date.UTC(2026, 4, 15);
const end = Date.UTC(2026, 5, 15);
const period = getPlanUsagePeriod(createSubscriptionPeriod(start, end), new Date(Date.UTC(2026, 5, 11)));
expect({
start: period.start.toISOString(),
end: period.end.toISOString(),
}).toMatchInlineSnapshot(`
{
"end": "2026-06-15T00:00:00.000Z",
"start": "2026-05-15T00:00:00.000Z",
}
`);
});
it("falls back to the current calendar month", () => {
const period = getPlanUsagePeriod(null, new Date(Date.UTC(2026, 5, 11, 12)));
expect({
start: period.start.toISOString(),
end: period.end.toISOString(),
}).toMatchInlineSnapshot(`
{
"end": "2026-07-01T00:00:00.000Z",
"start": "2026-06-01T00:00:00.000Z",
}
`);
});
});