fix(platform): address remaining should-fix items for rate-limit tiering

- Add docstring noting SubscriptionTier mirrors schema.prisma enum and
  can be replaced with prisma.enums import after prisma generate
- Remove unnecessary JSDoc comments from useRateLimitManager helpers
  per frontend code convention (avoid comments unless complex)
- Add audit trail: log old tier when admin changes a user's tier
- Fix stale test assertion (DEFAULT_TIER is FREE, not PRO)
- Show tier label ("Pro plan") in UsagePanelContent for end users
- Add formatResetTime unit tests (UsagePanelContent.test.ts)
- Add tier label display test in UsageLimits.test.tsx
- Fix pre-existing pyright errors from prisma stubs not having
  subscriptionTier (type: ignore until prisma generate is run)
This commit is contained in:
Zamil Majdy
2026-04-02 06:56:57 +02:00
parent 1de2a7fb09
commit 925e9a047c
9 changed files with 70 additions and 18 deletions

View File

@@ -188,10 +188,12 @@ async def set_user_rate_limit_tier(
admin_user_id: str = Security(get_user_id),
) -> UserTierResponse:
"""Set a user's rate-limit tier. Admin-only."""
old_tier = await get_user_tier(request.user_id)
logger.info(
"Admin %s setting tier for user %s to %s",
"Admin %s changing tier for user %s: %s -> %s",
admin_user_id,
request.user_id,
old_tier.value,
request.tier.value,
)
try:

View File

@@ -189,7 +189,7 @@ async def test_create_store_submission(mocker):
notifyOnAgentApproved=True,
notifyOnAgentRejected=True,
timezone="Europe/Delft",
subscriptionTier=prisma.enums.SubscriptionTier.FREE,
subscriptionTier=prisma.enums.SubscriptionTier.FREE, # type: ignore[reportCallIssue,reportAttributeAccessIssue]
)
mock_agent = prisma.models.AgentGraph(
id="agent-id",

View File

@@ -30,7 +30,13 @@ _USAGE_KEY_PREFIX = "copilot:usage"
class SubscriptionTier(str, Enum):
"""Subscription tiers with increasing token allowances."""
"""Subscription tiers with increasing token allowances.
Mirrors the ``SubscriptionTier`` enum in ``schema.prisma``.
Once ``prisma generate`` is run, this can be replaced with::
from prisma.enums import SubscriptionTier
"""
FREE = "FREE"
PRO = "PRO"
@@ -393,8 +399,8 @@ async def _fetch_user_tier(user_id: str) -> SubscriptionTier:
edge cases (e.g. partial row creation).
"""
user = await PrismaUser.prisma().find_unique(where={"id": user_id})
if user and user.subscriptionTier:
return SubscriptionTier(user.subscriptionTier)
if user and user.subscriptionTier: # type: ignore[reportAttributeAccessIssue]
return SubscriptionTier(user.subscriptionTier) # type: ignore[reportAttributeAccessIssue]
return DEFAULT_TIER

View File

@@ -369,8 +369,7 @@ class TestSubscriptionTier:
daily=UsageWindow(used=0, limit=100, resets_at=now + timedelta(hours=1)),
weekly=UsageWindow(used=0, limit=500, resets_at=now + timedelta(days=1)),
)
# Default tier should be PRO (beta testing default)
assert status.tier == SubscriptionTier.PRO
assert status.tier == SubscriptionTier.FREE
def test_usage_status_with_custom_tier(self):
now = datetime.now(UTC)

View File

@@ -16,18 +16,10 @@ export interface UserOption {
user_email: string;
}
/**
* Returns true when the input looks like a complete email address.
* Used to decide whether to call the direct email lookup endpoint
* vs. the broader user-history search.
*/
function looksLikeEmail(input: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input);
}
/**
* Returns true when the input looks like a UUID (user ID).
*/
function looksLikeUuid(input: string): boolean {
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
input,
@@ -43,7 +35,6 @@ export function useRateLimitManager() {
const [rateLimitData, setRateLimitData] =
useState<UserRateLimitResponse | null>(null);
/** Direct lookup by email or user ID via the rate-limit endpoint. */
async function handleDirectLookup(trimmed: string) {
setIsSearching(true);
setSearchResults([]);

View File

@@ -123,9 +123,20 @@ export function UsagePanelContent({
);
}
const tierLabel = usage.tier
? usage.tier.charAt(0) + usage.tier.slice(1).toLowerCase()
: null;
return (
<div className="flex flex-col gap-3">
<div className="text-xs font-semibold text-neutral-800">Usage limits</div>
<div className="flex items-baseline justify-between">
<span className="text-xs font-semibold text-neutral-800">
Usage limits
</span>
{tierLabel && (
<span className="text-[11px] text-neutral-500">{tierLabel} plan</span>
)}
</div>
{hasDailyLimit && (
<UsageBar
label="Today"

View File

@@ -31,16 +31,19 @@ function makeUsage({
dailyLimit = 10000,
weeklyUsed = 2000,
weeklyLimit = 50000,
tier = "FREE",
}: {
dailyUsed?: number;
dailyLimit?: number;
weeklyUsed?: number;
weeklyLimit?: number;
tier?: string;
} = {}) {
const future = new Date(Date.now() + 3600 * 1000); // 1h from now
return {
daily: { used: dailyUsed, limit: dailyLimit, resets_at: future },
weekly: { used: weeklyUsed, limit: weeklyLimit, resets_at: future },
tier,
};
}
@@ -110,6 +113,16 @@ describe("UsageLimits", () => {
expect(screen.getByText("100% used")).toBeDefined();
});
it("displays the user tier label", () => {
mockUseGetV2GetCopilotUsage.mockReturnValue({
data: makeUsage({ tier: "PRO" }),
isLoading: false,
});
render(<UsageLimits />);
expect(screen.getByText("Pro plan")).toBeDefined();
});
it("shows learn more link to credits page", () => {
mockUseGetV2GetCopilotUsage.mockReturnValue({
data: makeUsage(),

View File

@@ -0,0 +1,30 @@
import { describe, expect, it } from "vitest";
import { formatResetTime } from "../UsagePanelContent";
describe("formatResetTime", () => {
const now = new Date("2025-06-15T12:00:00Z");
it("returns 'now' when reset time is in the past", () => {
expect(formatResetTime("2025-06-15T11:00:00Z", now)).toBe("now");
});
it("returns minutes only when under 1 hour", () => {
const result = formatResetTime("2025-06-15T12:30:00Z", now);
expect(result).toBe("in 30m");
});
it("returns hours and minutes when under 24 hours", () => {
const result = formatResetTime("2025-06-15T16:45:00Z", now);
expect(result).toBe("in 4h 45m");
});
it("returns formatted date when over 24 hours away", () => {
const result = formatResetTime("2025-06-17T00:00:00Z", now);
expect(result).toMatch(/Tue/);
});
it("accepts a Date object for resetsAt", () => {
const resetDate = new Date("2025-06-15T14:00:00Z");
expect(formatResetTime(resetDate, now)).toBe("in 2h 0m");
});
});

View File

@@ -13127,7 +13127,7 @@
"type": "string",
"enum": ["FREE", "PRO", "BUSINESS", "ENTERPRISE"],
"title": "SubscriptionTier",
"description": "Subscription tiers with increasing token allowances."
"description": "Subscription tiers with increasing token allowances.\n\nMirrors the ``SubscriptionTier`` enum in ``schema.prisma``.\nOnce ``prisma generate`` is run, this can be replaced with::\n\n from prisma.enums import SubscriptionTier"
},
"SuggestedGoalResponse": {
"properties": {