Add an admin history view for resolved upgrade requests - #29628
Conversation
Admins could only ever see pending upgrade requests — once resolved, a request vanished from the UI with no way to see who requested what, who approved/denied it, or whether the resulting grant is still active. Adds grantedAwuCredits/grantedExpiresAt/expiredAt to membership_upgrade_requests: a snapshot of what was actually granted when a request is approved via the linked "Edit limit" flow. setUserSpendLimit now closes out any previously-tracked grant for a member before applying a new override — whether or not the new save is itself request-linked, since the override is a single overwritable slot and any change supersedes what came before. The expiration sweep does the same when it reverts an override naturally. Admins can now switch to a "History" tab on the requests table showing requester, reason, resolver, resolution time, and the grant's status (active/expired/superseded/none). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
| {status === "approved" ? ( | ||
| <Chip size="xs" color="success" label="Approved" /> | ||
| ) : ( | ||
| <Chip size="xs" color="warning" label="Denied" /> |
There was a problem hiding this comment.
[GEN6] Prefer exhaustive switch + assertNever over if/else on union types
status is typed as MembershipUpgradeRequestStatus (a string union that includes at least "pending" | "approved" | "denied"). Using a ternary here skips TypeScript's exhaustiveness check — if a new status value is added, this silently falls through to the Denied chip. Prefer an exhaustive switch with assertNeverAndIgnore (API data, client-side):
| <Chip size="xs" color="warning" label="Denied" /> | |
| {(() => { | |
| switch (status) { | |
| case "approved": | |
| return <Chip size="xs" color="success" label="Approved" />; | |
| case "denied": | |
| return <Chip size="xs" color="warning" label="Denied" />; | |
| default: | |
| assertNeverAndIgnore(status); | |
| return null; | |
| } | |
| })()} |
| app.get( | ||
| "/", | ||
| ensureIsManager(), | ||
| validate("query", ListUpgradeRequestsQuerySchema), |
There was a problem hiding this comment.
[BACK14] Keep Swagger documentation in sync with API schema changes
This endpoint's request schema now accepts a new ?status query parameter, and the response shape also gains new fields (resolvedBy, grantedAwuCredits, grantedExpiresAt, expiredAt) via toJSON(). Please verify the @swagger or @ignoreswagger annotation in this file (and swagger_private_schemas.ts if the response type is referenced there) is up to date.
| // be snapshotted onto that request for the admin history view. | ||
| const UpdateUserSpendLimitBodySchema = z.discriminatedUnion("kind", [ | ||
| z.object({ kind: z.literal("unlimited") }), | ||
| z.object({ kind: z.literal("unlimited"), requestId: z.string().optional() }), |
There was a problem hiding this comment.
[BACK14] Keep Swagger documentation in sync with API schema changes
The request body schema now accepts an optional requestId field. Please verify the @swagger or @ignoreswagger annotation in this file (and any shared private schema) reflects this addition.
Summary
grantedAwuCredits/grantedExpiresAt/expiredAttomembership_upgrade_requests: a snapshot of what was actually granted, taken when a request is approved through the linked "Edit limit" flow (recordGrant).setUserSpendLimitnow closes out any previously-tracked grant for that member (expireActiveGrantsForUser) before applying any new override, whether or not the new save is itself request-linked. The expiration sweep does the same when it reverts naturally. This means a request's history row accurately reflects "expired naturally" vs. "superseded early by a later change" — both surface as the grant no longer being active, with the actual end date.Test plan
npm run test -- lib/api/users/spend_limit.test.ts(front)npm run test -- temporal/spend_limit_expiration/activities.test.ts(front) — sweep stampsexpiredAton the linked request.npm run test -- "members/[uId]/spend_limit.test.ts"(front-api) — grant snapshot + supersede-on-new-save.npm run test -- "credits/upgrade-requests.test.ts"(front-api) —?status=resolvedhistory listing.Deploy plan
🤖 Generated with Claude Code